Skip to content

Instantly share code, notes, and snippets.

@tokland
Last active May 23, 2021 15:39
Show Gist options
  • Save tokland/0c07728c47b717c6c35d4fa107498691 to your computer and use it in GitHub Desktop.
Save tokland/0c07728c47b717c6c35d4fa107498691 to your computer and use it in GitHub Desktop.
Example of immutable get/set using nested and composable selectors/lenses
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