Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
qpwo opened this issue Oct 6, 2021 · 9 comments

Comments

@qpwo
Copy link

qpwo commented Oct 6, 2021

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
Copy link
Author

qpwo commented Oct 6, 2021

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
Copy link
Owner

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
Copy link
Author

qpwo commented Oct 15, 2021

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.

@qpwo
Copy link
Author

qpwo commented Oct 15, 2021

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
Copy link
Author

qpwo commented Oct 15, 2021

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

@Yomguithereal
Copy link
Owner

@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
Copy link
Author

qpwo commented Oct 22, 2021

Yeah I'll give it a try

@qpwo
Copy link
Author

qpwo commented Oct 31, 2021

Okay giving it a shot today

@qpwo
Copy link
Author

qpwo commented Nov 4, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants