microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.13k stars 12.5k forks source link

PathIn<T> type just like Partial<T> type for deep field path in a object. #23398

Closed AmitDigga closed 6 years ago

AmitDigga commented 6 years ago

I love typescript wanted to ask if this can be a feature. I searched some issues related to this but could not find any help.

Sorry for my noobness.

For model Car

interface Car {
    engine: {
        name: string;
        modelNumber: string;
    },
    body:{
        color:string;
        shape:{
        }
    },
}

Current problem

Describing nested or deep typings needs to be improved. Below code is taken from ngrx/store

class Store<T>{
    // ...
    select<a extends keyof T>(key: a): Store<T[a]>;
    select<a extends keyof T, b extends keyof T[a]>(key1: a, key2: b): Store<T[a][b]>;
    select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b]>(key1: a, key2: b, key3: c): Store<T[a][b][c]>;
    select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c]>(key1: a, key2: b, key3: c, key4: d): Store<T[a][b][c][d]>;
    select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c], e extends keyof T[a][b][c][d]>(key1: a, key2: b, key3: c, key4: d, key5: e): Store<T[a][b][c][d][e]>;
    select<a extends keyof T, b extends keyof T[a], c extends keyof T[a][b], d extends keyof T[a][b][c], e extends keyof T[a][b][c][d], f extends keyof T[a][b][c][d][e]>(key1: a, key2: b, key3: c, key4: d, key5: e, key6: f): Store<T[a][b][c][d][e][f]>;
    // ...
}

So select method can be used like this

let store :  Store<Car>;
store.select('engine','name');
  1. Lot of boilerplate and still do not cover all cases, only upto 6 keys. Not productive.
  2. Javascript object is multi level object and keyof restricts us to only one level.

My Suggestion

There should be a way to address deep field or nested path in the language itself. I will be discussing PathIn<T> type like Partial<T>, but it can any other thing like pathin as keyof etc. (this is beyond my knowledge) let path: PathIn<Car>; So path should be limited to ['engine'], ['engine', 'name'], ['engine', 'modelNumber'], ['body'], ['body', 'color'] and ['body', 'shape'].

For single level it may be 'engine' or 'body' i.e. without array notation. This is just a sugar syntax, not required.

let path1 :PathIn<Car> = ['engine']; //Okay
let path2 :PathIn<Car> = ['abc']; //Error

Since PathIn only means multilevel path so for

let pathToBodyColor : PathIn<Car> = ['body','color'];
  1. type of Car[pathToBodyColor] should be string.
  2. For let car:Car, one of these should return value at car.body.color
    • car[pathToBodyColor] === car.body.color
    • pathToBodyColor[car] === car.body.color
    • pathToBodyColor.from(car) === car.body.color
  3. let emptyPath: Path<T> = []; is not clear.
    • If valid, then it can be valid for every T in number, undefined, null , {}etc. and as discussed in point 2 emptyPath.from("abc") should return "abc" itself.
    • If invalid, then Path<T> where T is in number, undefined, null , {} etc. should not be possible.
  4. For let path : PathIn<any> , the path array can be anything which is a possible valid path.

And now the above code for Store.select() can be

class Store<T>{
    // ...
    select<a extends PathIn<T>>(key: a): Store<T[a]>;
    // ...
}

Extra

Dealing with object like

interface Car2 {
    name:string;
    bodyColor:string;
    bodyShape:Polygon;
    bodyWeight:number;
    bodyDimension: Box;
    engineModel:string;
    engineManufacturedOn:string,
}

then there is no problem, but we know in real world objects are not like that and keyof is very limited.

krryan commented 6 years ago

Partial is built-in for convenience, but it's trivial to implement yourself. It doesn't have any special or hard-coded rules. So you could, in theory, produce your PathIn type yourself.

However, there isn't currently any way to modify tuples as types. So you can write a type that finds ['engine'], and notices that Car['engine'] is an object, and then recurses and finds ['name'] (well, at least an interface that does that; see below), but no way to join ['engine'] and ['name'] to be ['engine', 'name'].

But if you were willing to accept other structures for representing this path, that wouldn't be a problem. For example, a linked list would be easy enough to do.

The problem then is that Typescript does not support types that circularly reference themselves. Classes and interfaces can do it, and that's the recommended workaround, but a single interface cannot somehow be a union of anything. Types support distribution across a union, which you need for this.

Given the advent of conditional types, perhaps the eager resolution of types, and therefore their inability to handle circular references, should be revisited.

jack-williams commented 6 years ago

I think things like Car[pathToBodyColor] where pathToBodyColor is a tuple is abusing index syntax too much IMO. Anyway, here is a PathIn<T> type I had a go at.

The best I can do; expect this to blow up:

type Append<X, T extends any[]> =
    T extends [infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8, infer A9] ?
    [A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, X] :

    T extends [infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8] ?
    [A0, A1, A2, A3, A4, A5, A6, A7, A8, X] :

    T extends [infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7] ?
    [A0, A1, A2, A3, A4, A5, A6, A7, X] :

    T extends [infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6] ?
    [A0, A1, A2, A3, A4, A5, A6, X] :

    T extends [infer A0, infer A1, infer A2, infer A3, infer A4, infer A5] ?
    [A0, A1, A2, A3, A4, A5, X] :

    T extends [infer A0, infer A1, infer A2, infer A3, infer A4] ?
    [A0, A1, A2, A3, A4, X] :

    T extends [infer A0, infer A1, infer A2, infer A3] ?
    [A0, A1, A2, A3, X] :

    T extends [infer A0, infer A1, infer A2] ?
    [A0, A1, A2, X] :

    T extends [infer A0, infer A1] ?
    [A0, A1, X] :

    T extends [infer A0] ?
    [A0, X] : never

type TraverseTop<T> =
    T extends object ?
    { [K in keyof T]: [K] | Traverse<[K],T[K]> }
    : never

type Traverse<P extends any[],T> =
    T extends object ?
    keyof T extends never ? P : 
    { [K in keyof T]: P | Traverse<Append<K,P>,T[K]> }
    : P

type Flatten10<T> = T extends any[] ? T : T extends object ? Flatten9<T[keyof T]> : T;
type Flatten9<T> = T extends any[] ? T : T extends object ? Flatten8<T[keyof T]> : T;
type Flatten8<T> = T extends any[] ? T : T extends object ? Flatten7<T[keyof T]> : T;
type Flatten7<T> = T extends any[] ? T : T extends object ? Flatten6<T[keyof T]> : T;
type Flatten6<T> = T extends any[] ? T : T extends object ? Flatten5<T[keyof T]> : T;
type Flatten5<T> = T extends any[] ? T : T extends object ? Flatten4<T[keyof T]> : T;
type Flatten4<T> = T extends any[] ? T : T extends object ? Flatten3<T[keyof T]> : T;
type Flatten3<T> = T extends any[] ? T : T extends object ? Flatten2<T[keyof T]> : T;
type Flatten2<T> = T extends any[] ? T : T extends object ? Flatten1<T[keyof T]> : T;
type Flatten1<T> = T extends any[] ? T : T extends object ? Flatten0<T[keyof T]> : T;
type Flatten0<T> = T

interface Car {
    engine: {
        name: string;
        modelNumber: string;
    },
    body:{
        color:string;
        shape:{
        }
    },
}

type PathIn<T> = Flatten10<TraverseTop<T>>;
let path1:PathIn<Car> = ['engine']; //Okay
let path2:PathIn<Car> = ['abc']; //Error
let pathToBodyColor: PathIn<Car> = ['body','color'];
typescript-bot commented 6 years ago

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.