Yomguithereal / baobab

JavaScript & TypeScript persistent and optionally immutable data tree with cursors.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Generic types for get, set, select, and apply; immutability for apply callback input

qpwo opened this issue · comments

It's nice to catch typos in keys in selectors, know the type of selected data, etc. I'll paste in my code so far here because it's not ready for a PR or anything yet. Figured I'd ask before trying to make it tidy and work well in a proper PR, do you want this functionality in the repo?

Catching typos:

image

Return types on get:

image

Immutability for apply callback:

image

Full code follows. still has some bugs. Will improve if there's interest.

// @ts-nocheck
import Baobab, { BaobabOptions, Cursor } from 'baobab'
import type Immutable from './immutable'
interface EmptyInterface { }

// https://stackoverflow.com/a/65963590
type PathTree<T> = {
    [P in keyof T]-?: T[P] extends object
    ? [P] | [P, ...AllPaths<T[P]>]
    : [P]
}

type AllPaths<T> = PathTree<T>[keyof PathTree<T>]


// https://stackoverflow.com/a/61648690
type DeepIndex<T, KS extends Keys, Fail = undefined> =
    KS extends [infer F, ...infer R] ? F extends keyof T ? R extends Keys ?
    DeepIndex<T[F], R, Fail> : Fail : Fail : T

type Keys = readonly PropertyKey[]


// https://stackoverflow.com/a/58993872/4941530
type ImmutablePrimitive = undefined | null | boolean | string | number | Function

type Immutable<T> =
    T extends ImmutablePrimitive ? T :
    T extends Array<infer U> ? ImmutableArray<U> :
    T extends Map<infer K, infer V> ? ImmutableMap<K, V> :
    T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>

type ImmutableArray<T> = ReadonlyArray<Immutable<T>>
type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>
type ImmutableSet<T> = ReadonlySet<Immutable<T>>
type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }


export class MyBaobab<T extends EmptyInterface> extends Baobab {
    constructor(initialState?: T, options?: Partial<BaobabOptions>) {
        super(initialState, options)
    }

    root: MyCursor<T>
    options: BaobabOptions

    apply(getNew: (state: T) => T): T { return super.apply(path, getNew) }
    apply<K extends keyof T>(path: K, getNew: (state: Immutable<T[K]>) => Immutable<T[K]>): MyCursor<T[K]>
    apply<K extends AllPaths<T>>(path: K, getNew: (state: Immutable<T[K]>) => Immutable<T[K]>): DeepIndex<T, K> { return super.apply(path, getNew) }
    // apply<K extends keyof T>(path: K, getNew: (state: T[K]) => T[K]): MyCursor<T[K]>
    // apply<K extends AllPaths<T>>(path: K, getNew: (state: T[K]) => T[K]): DeepIndex<T, K> { return super.apply(path, getNew) }

    select<K extends keyof T>(path: K): MyCursor<T[K]>
    select<K extends AllPaths<T>>(path: K): MyCursor<DeepIndex<T, K>> { return super.select(path) }

    set(value: T): T { return super.set(value) }
    set<K extends keyof T>(path: K, value: T[K]): T[K]
    set<K extends AllPaths<T>>(path: K, value: T[K]): DeepIndex<T, K> { return super.set(path, value) }

    get(): T { return super.get() }
    get<K extends keyof T>(path: K): T[K]
    get<K extends AllPaths<T>>(path: K): DeepIndex<T, K> { return super.apply(path) }
}


export class MyCursor<T extends EmptyInterface> extends Cursor {
    constructor() { super() }
    apply(getNew: (state: T) => T): T { return super.apply(path, getNew) }
    apply<K extends keyof T>(path: K, getNew: (state: Immutable<T[K]>) => Immutable<T[K]>): MyCursor<T[K]>
    apply<K extends AllPaths<T>>(path: K, getNew: (state: Immutable<T[K]>) => Immutable<T[K]>): DeepIndex<T, K> { return super.apply(path, getNew) }

    select<K extends keyof T>(path: K): MyCursor<T[K]>
    select<K extends AllPaths<T>>(path: K): MyCursor<DeepIndex<T, K>> { return super.select(path) }

    set(value: T): T { return super.set(value) }
    set<K extends keyof T>(path: K, value: T[K]): T[K]
    set<K extends AllPaths<T>>(path: K, value: T[K]): DeepIndex<T, K> { return super.set(path, value) }

    get(): T { return super.get() }
    get<K extends keyof T>(path: K): T[K]
    get<K extends AllPaths<T>>(path: K): DeepIndex<T, K> { return super.apply(path) }

}

Could also add a ReadOnlyCursor generic type. Then a caller can cast a cursor before giving it to callee, and ensure callee does not set any data.

From afar it looks nice but I must admit I am not skilled enough in TS to know whether it would mess up with other people's code. @jacomyal any insights?

Very likely to cause typescript build errors in any codebases that have some unknown (likely irrelevant) type errors in their baobab tree. Users may want to fix those type errors but would be a pain in the ass if you're not active on a project and just wanted to update dependencies.

Two options:

  • A major version change would prevent npm update from triggering the change but then new bugfixes won't go through either unless you maintain both versions.
  • Export something like TBoabab and TCursor from the package for the more-strongly-typed version, then people who want that can use it intentionally. They should notice it in the auto-suggest when they start typing the import.

Oh also some packages (e.g. lodash.get) explicitly type out 7 or so levels deep the return types for everything. I think it's slightly faster than this DeepIndex and PathTree stuff and won't cause any problems in recursive data structures, so that may be a better option. Only thing is that if you have a bunch of methods (get, set, apply, select, on('listen',…), etc) then it's quite a lot of code. Could be auto-generated but that's also a bag of worms.

All that said I still think I could take the time to do this PR proper if yall decide you'd use it

Oh you or I could also publish as a separate npm package.

@qpwo I think it would be a good first step to test the water indeed. I guess you would release something like the code presented above?

Yeah I'll give it a try

Okay giving it a shot today

Some reason didn't get linked. This is PR #513