Last active
April 26, 2019 19:49
-
-
Save spitz-dan-l/be8895c070c710c12f09571f31c57b15 to your computer and use it in GitHub Desktop.
update.ts - Concise, typed immutable updates to deeply-nested objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
update.ts - Concise, typed immutable updates to deeply-nested objects | |
Daniel Spitz | |
Use it like this: | |
import {update} from 'update'; | |
let obj = { a: 3, b: 'horse', c: { d: [1,2,3] }}; | |
let obj2 = update(obj, { a: 0, c: { d: _ => [..._, 4] } }); | |
Concise: you only need to type the keys being updated once. | |
Typed: It typechecks the second argument. It infers the types of all nested update values and updater functions. | |
Immutable: Doesn't modify the original object. Reuses substructures without copying where possible. | |
In the above example, the function in the 'd' property is an updater function; it will be | |
called with the old value of d, and the result will be used to replace it in obj2. | |
Inspired indirectly by discussions here | |
https://github.com/Microsoft/TypeScript/issues/13923 | |
in which it is discussed how to use recursive mapped types to enforce immutability | |
through a deeply-nested object | |
Issues: | |
- Awkward to update embedded functions. You are forced to always supply an updater function for them. | |
(Otherwise, it would be ambiguous at runtime whether you had supplied a replacement or an updater function.) | |
- Does not deal with Maps or Sets in a useful way (ignores their keys, looks at their object properties) | |
(because I don't use them because they don't support compound keys) | |
- The type signature of update() uses two type parameters, when only one really ought to be necessary. | |
- Behavior for "unique symbol" types is finnicky | |
- It should be possible to specify a new typing of this, with the same underlying impl, | |
which can transform from the source type to a new target type | |
I strongly encourage you to stake your professional reputation on the behavior of this code. | |
*/ | |
export type Updater<T> = | |
// Wrapping in [] makes typescript not distribute unions down the tree (seems pretty dumb to me) | |
// See discussion here: https://github.com/Microsoft/TypeScript/issues/22596 | |
[T] extends [NotFunction<T>] ? | |
(T extends Primitive | any[] ? T : | |
T extends object ? ObjectUpdater<T> : | |
never) | | |
((x: T) => T) : | |
(x: T) => T; | |
type NotFunction<T> = T extends (...args: any) => any ? never : T; | |
type Primitive = undefined | null | boolean | string | number | symbol; | |
type ObjectUpdater<T> = { | |
[K in keyof T]?: Updater<T[K]> | |
} | |
// The second generic type parameter is a hack to prevent typescript from using the contents of updater | |
// to figure out the source and return types when doing type inference on calls to this function. | |
export function update<S, U extends S=S>(source: S, updater: Updater<U>): S { | |
// if updater is a function, call it and return the result | |
if (updater instanceof Function) { | |
return <S>(<Function> updater)(source); | |
} | |
// if updater is a non-traversible value | |
// check for all types we don't intend to recursively traverse. | |
// this means all (non-function) primitives, and arrays | |
if ( !(updater instanceof Object) || updater instanceof Array ) { | |
return <S>updater; | |
} | |
// updater is an Object, traverse each key/value and update recursively. | |
// note: if you are just trying to set to a deeply-nested object with no traversal, | |
// you can achieve this by passing a function returning your desired object. | |
if (updater instanceof Object) { | |
let result: Partial<S>; | |
if (source instanceof Object) { | |
result = {...source}; | |
} else { | |
result = {}; | |
} | |
for (let [n, v] of Object.entries(updater)) { | |
if (v === undefined) { | |
delete result[n]; | |
} else { | |
result[n] = update(result[n], v); | |
} | |
} | |
return <S>result; | |
} | |
throw Error('Should never get here'); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment