Skip to content

Instantly share code, notes, and snippets.

@tokland
Last active May 23, 2021 15:39
Show Gist options
  • Select an option

  • Save tokland/0c07728c47b717c6c35d4fa107498691 to your computer and use it in GitHub Desktop.

Select an option

Save tokland/0c07728c47b717c6c35d4fa107498691 to your computer and use it in GitHub Desktop.

Revisions

  1. tokland revised this gist May 23, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion immutable-selectors.ts
    Original file line number Diff line number Diff line change
    @@ -98,7 +98,7 @@ const personS = selector<Person>();
    const addressS = selector<Address>();

    const person: Person = {
    name: "Mary Cassat",
    name: "Mary Cassatt",
    age: 35,
    address: { street: { name: "Painters St" }, number: 1 },
    };
  2. tokland created this gist May 22, 2021.
    122 changes: 122 additions & 0 deletions immutable-selectors.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,122 @@
    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 Cassat",
    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);