// type UpdaterFnParam<T> = ((prev: T) => T);
export type UpdaterFn<T> = (arg: UpdaterFnParam<T>) => void;
export type UpdaterGeneratorType<T> = {
[k in keyof T]: T[k] extends { [kkt: string]: any }
? (UpdaterGeneratorType<T[k]> & {
getUpdater: () => UpdaterFn<T[k]>,
set: UpdaterFn<T[k]>,
update: UpdaterFn<T[k]>,
})
: {
getUpdater: () => UpdaterFn<T[k]>,
set: UpdaterFn<T[k]>,
update: UpdaterFn<T[k]>,
}
} & {
getUpdater: () => UpdaterFn<T>,
set: UpdaterFn<T>,
update: UpdaterFn<T>,
}
/**
* Convenience method for generating setState<FancyObject.sub.component>() from setState<FancyObject> callbacks.
* If used in react, recommended that this be memoized.
*
* @generic T should be a data-only object - nested objects are allowed but arrays, sets not supported
* @param dataObject ANY instance of T, used only for its keys. MUST have all keys present
* @param dataUpdater an updater function, which can be called as: dataUpdater(newT) or
* dataUpdater((oldT) => { return newTFromOldT(oldT) }) ; e.g. react setState() function.
* @return a deep object that has the same keys as T, except each key also has a getUpdater()/set/update member;
* the getUpdater() on a subobject of T acts similarly to the dataUpdater<T> but to the subobject rather than the whole object.
* e.g. :
* let gameStateUpdater = updaterGenerator(skeletonObject, setGameState);
* let setName = gameStateUpdater.player.name.getUpdater();
* gameStateUpdater.player.name.set(newName);
* gameStateUpdater.player.name.update(oldName => oldName + " ");
*
*/
export function updaterGenerator<T>(dataObject: T, dataUpdater: UpdaterFn<T>): UpdaterGeneratorType<T> {
const updaters: UpdaterGeneratorType<T> = {} as any;
updaters.getUpdater = () => dataUpdater;
updaters.set = dataUpdater;
updaters.update = dataUpdater;
if (typeof dataObject !== "object") return updaters;
const keys : (keyof T)[] = Object.keys(dataObject) as any as (keyof T)[];
keys.forEach((key: (keyof T)) => {
if (key === "set" || key === "getUpdater" || key === "update") {
throw Error(`Invalid key in updaterGenerator: ${key} conflicts with reserved keywords set, update, getUpdater.`);
}
function keyUpdater(newValueOrCallback: UpdaterFnParam<T[typeof key]>) {
if (typeof newValueOrCallback === "function") {
dataUpdater((oldData) => {
const newData = {
...oldData,
[key]: (newValueOrCallback as Function)(oldData[key]),
};
return newData;
});
} else {
dataUpdater((oldData) => ({ ...oldData, [key]: newValueOrCallback }));
}
}
updaters[key] = updaterGenerator(dataObject[key], keyUpdater) as any;
});
return updaters;
}