Yomguithereal / baobab

JavaScript & TypeScript persistent and optionally immutable data tree with cursors.
MIT License
3.15k stars 117 forks source link

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

Open qpwo opened 3 years ago

qpwo commented 3 years ago

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) }

}
qpwo commented 3 years ago

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.

Yomguithereal commented 3 years ago

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?

qpwo commented 3 years ago

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:

qpwo commented 3 years ago

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

qpwo commented 3 years ago

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

Yomguithereal commented 3 years ago

@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?

qpwo commented 3 years ago

Yeah I'll give it a try

qpwo commented 3 years ago

Okay giving it a shot today

qpwo commented 3 years ago

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