Last active
May 23, 2021 15:39
-
-
Save tokland/0c07728c47b717c6c35d4fa107498691 to your computer and use it in GitHub Desktop.
Example of immutable get/set using nested and composable selectors/lenses
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
type IsArray<T> = T extends unknown[] ? true : false; | |
type IsObject<T> = T extends object ? (IsArray<T> extends true ? false : true) : false; | |
type ObjectS<From, To> = { | |
[K in keyof To]: Selector<From, To[K]>; | |
}; | |
interface BaseFns<From, To> { | |
get: (from: From) => To; | |
set: (from: From, value: To) => From; | |
} | |
interface Fns<From, To> extends BaseFns<From, To> { | |
compose<To2>(selector2: Selector<To, To2>): Selector<From, To2>; | |
} | |
type Accessors<From, To> = IsObject<To> extends true ? ObjectS<From, To> : {}; | |
type Selector<From, To> = Accessors<From, To> & { _: Fns<From, To> }; | |
function buildFns<From, To>(fns1: BaseFns<From, To>): Fns<From, To> { | |
return { | |
...fns1, | |
compose<To2>(selector2: Selector<To, To2>) { | |
return composeFns(fns1, selector2._); | |
}, | |
}; | |
} | |
function composeFns<From, To1, To2>( | |
fns1: BaseFns<From, To1>, | |
fns2: BaseFns<To1, To2> | |
): Selector<From, To2> { | |
return getProxy<From, To2>({ | |
get: obj => { | |
// <- selector1 -> <---- selector2 ---> <-obj-> | |
// compose(personS.address, addressS.street.name)._.get(person); | |
const obj2 = fns1.get(obj); | |
return fns2.get(obj2); | |
}, | |
set: (obj, value) => { | |
// <- selector1 -> <---- selector2 ---> <-obj-> <-value-> | |
// compose(personS.address, addressS.street.name)._.set(person, "Elm St"); | |
const obj2 = fns1.get(obj); | |
const obj2Updated = fns2.set(obj2, value); | |
return fns1.set(obj, obj2Updated); | |
}, | |
}); | |
} | |
function getProxy<From, To>(fns: BaseFns<From, To>): Selector<From, To> { | |
const handler: ProxyHandler<Selector<From, To>> = { | |
get(_target, prop_, _receiver) { | |
if (prop_ === "_") { | |
return buildFns(fns); | |
} else { | |
const prop = prop_ as keyof To; | |
const fnsForObject: BaseFns<To, To[typeof prop]> = { | |
get: obj => obj[prop], | |
set: (obj, value) => ({ ...obj, [prop]: value }), | |
}; | |
return composeFns(fns, fnsForObject); | |
} | |
}, | |
}; | |
const selector = { _: fns } as Selector<From, To>; | |
return new Proxy(selector, handler); | |
} | |
// Public interface | |
export function selector<T extends object>(): Selector<T, T> { | |
const identityFns: BaseFns<T, T> = { | |
get: obj => obj, | |
set: (_obj, value) => value, | |
}; | |
return getProxy(identityFns); | |
} | |
export function compose<From, To1, To2>( | |
selector1: Selector<From, To1>, | |
selector2: Selector<To1, To2> | |
): Selector<From, To2> { | |
return composeFns(selector1._, selector2._); | |
} | |
/* Example */ | |
type Person = { name: string; age: number; address: Address }; | |
type Address = { street: { name: string }; number: number }; | |
const personS = selector<Person>(); | |
const addressS = selector<Address>(); | |
const person: Person = { | |
name: "Mary Cassatt", | |
age: 35, | |
address: { street: { name: "Painters St" }, number: 1 }, | |
}; | |
// Single selector | |
const streenNameS1 = personS.address.street.name; | |
const streetName1 = streenNameS1._.get(person); | |
const person1 = streenNameS1._.set(person, "Elm St 1"); | |
console.log(streetName1, person1); | |
// Composing selectors 1 | |
const streetNameS2 = personS.address._.compose(addressS.street.name); | |
const streetName2 = streetNameS2._.get(person); | |
const person2 = streetNameS2._.set(person, "Elm St 2"); | |
console.log(streetName2, person2); | |
// Composing selectors 2 | |
const streetNameS3 = compose(personS.address, addressS.street.name); | |
const streetName3 = streetNameS3._.get(person); | |
const person3 = streetNameS2._.set(person, "Elm St 3"); | |
console.log(streetName3, person3); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment