Recently introduces keyof
operator will work well for typing functions that accept properties directly on target type.
interface Some {
a: number,
b: string
}
type oneOfTheSomeKeys = keyof Some // restricts value to "a", "b"
What do you think, is it possible in theory to type deeply nested paths, so that for type:
interface Some {
a: number,
b: {
c: number
d: {
e: number
}
}
}
so that it would be possible to restrict possible path values to:
["a"]
["b"]
["b", "c"]
["b", "d"]
["b", "d, "e"]
This actual for such methods like ramda's path
:
R.path(['a', 'b'], {a: {b: 2}}); //=> 2
R.path(['a', 'b'], {c: {b: 2}}); //=> undefined
?
You can already do that with manual overloading.
// overload more type parameter arity here
function path<A extends string, B extends string, C extends string, D>(path: [A, B, C], d: {
[K1 in A]: {[K2 in B]: {[K3 in C]: D}}
}) {
let ret = d
for (let k of path) {
ret = ret[k as string]
}
}
path(['a', 'b', 'c'], {a: {b: 2}}) // error
path(['a', 'b', 'c'], {a: {b: {c: 2}}})
@HerringtonDarkholme oh that is really really nice, thanks for the example!
@HerringtonDarkholme very slick
@HerringtonDarkholme: thank you, that's pretty cool! I generated variants for different path lengths for Ramda, so if you'd like to use it, feel free. :smile:
I tried to see if I could make your definition work with arrays as well, but so far I haven't had much luck.
Cases I'm hoping will work (or at least one of the two):
path(['a', '0', 'c'], {a: [{c: 2}] })
path(['a', 0, 'c'], {a: [{c: 2}] })
I tried to see if adjusting the definition might help to make this work out.
function path<A extends string, B extends number, C extends string, D>(path: [A, B, C], d: { // <- changed B to number
[K1 in A]: {[K2 in B]: {[K3 in C]: D}} // <- `K2 in B` now errors: "Type 'number' is not assignable to type 'string'"
}) {
// implementation detail
}
I suppose with {[K2 in B]: ...}
this is being considered an object using a string-based index, making numerical indices (as used by arrays) fail. Perhaps this is implied by the in
notation?
@tycho01
To make this:
path(['a', '0', 'c'], { a: [{ c: 2 }] })
path(['a', 0, 'c'], { a: [{ c: 2 }] })
work, typings should be something like that:
// for level 1 array
function path<A extends string, B extends string | number, C extends string, D>
(path: [A, B, C],
d: {[K1 in A]: {[K2 in C]: D}[]}
): D
// for object
function path<A extends string, B extends string | number, C extends string, D>
(path: [A, B, C],
d: {[K1 in A]: {[K2 in B]: {[K3 in C]: D}}}
): D
function path(path: any, d: any): any {
let ret = d
for (let k of path) {
ret = ret[k]
}
}
The problem that it is not possible to do it withing a single signature for example like:
d: {[K1 in A]: {[K2 in B]: {[K3 in C]: D}}} | {[K1 in A]: {[K2 in C]: D}[]}
So if to still implement it there would be a need to have multiple combinations:
A: O : O : O
A: A : O : O
...
O: A : O : O
...
You get it. But it is not impossible though.
Yeah, the exploding number of variants is a tad unfortunate, but for the moment should do if I can try to generate them.
I guess technically the []
approach still presents an asymmetry between the objects and array versions, by constraining the arrays to be homogeneous lists, as opposed to say tuples, while the objects do not appear bound that way.
That said, this progress is pretty great! I'll try to incorporate your idea for the Ramda function. :D
Yes it seem that is also case with tuples which makes typing issue unsolvable for general case.
@whitecolor: made a commit for path lengths 1~7 (-> index.d.ts
). Lotta code to get one extra test to pass, with still dozens others failing (not to mention the ones silently 'failing' with any types). Worth it!
Can't wait to see what it'd look like if it is to handle tuples as well!
@HerringtonDarkholme or somebody
any advice how this can be typed, function that gets two key names and object and checks if first key is string, and second is number:
function checkTypesOfKeys<
KeyS extends string, KeyN extends string>
(keyS: KeyS, keyN: KeyN,
obj: {[K in KeyS]: string} & {[K in KeyN ]: number}): boolean // this doesn't work
{
return typeof (<any>obj)[keyS] === 'string'
&& typeof (<any>obj)[keyN] === 'number'
}
checkTypesOfKeys('str', 'num', {str: 'stringValue', num: 1}) // error
Type '{ str: string; num: number; }' is not assignable to type '{ str: string; num: string; }'.
@whitecolor: what if you make that KeyN
into number
?
On that path
definition I generated for Ramda based on the overloading suggestion given here, I've come to the conclusion this not only brings monstrous typings that are still inadequate (ignoring tuples), but also brings along performance issues, grinding TS to a halt after adding a significant number of overloads. Evidently, just following the original reduce
logic is O(n)
, so I hope they'll consider my proposal to implement that as a solution...
KeyN
in my question I assume to correspond to second key name (key name is a string anyway) argument.
I hope they'll consider my proposal to implement that as a solution...
In 10 years maybe =)
@whitecolor
The problem here is how TypeScript infer type argument. The KeyN
and KeyM
will be inferred against { str: string; num: number; }
so that compiler will infer types compatible both with extends string
and keyof typeof obj
.
In such condition, KeyN
is inferred as str | num
. So the error.
One solution is using curry to help compiler infer type argument.
https://www.typescriptlang.org/play/index.html#src=function%20checkTypesOfKeys%3C%0D%0A%20%20KeyS%20extends%20string%2C%20KeyN%20extends%20string%3E%0D%0A%20%20(keyS%3A%20KeyS%2C%20keyN%3A%20KeyN)%3A%20(obj%3A%20%7B%5BK%20in%20KeyS%5D%3A%20string%7D%20%26%20%7B%5BK%20in%20KeyN%20%5D%3A%20number%7D)%20%3D%3E%20boolean%20%2F%2F%20this%20doesn%27t%20work%0D%0A%7B%0D%0A%20%20return%20(obj)%20%3D%3E%20typeof%20(%3Cany%3Eobj)%5BkeyS%5D%20%3D%3D%3D%20%27string%27%0D%0A%20%20%20%20%26%26%20typeof%20(%3Cany%3Eobj)%5BkeyN%5D%20%3D%3D%3D%20%27number%27%0D%0A%7D%0D%0A%0D%0AcheckTypesOfKeys(%27str%27%2C%20%27num%27)(%7Bstr%3A%20%27stringValue%27%2C%20num%3A%201%7D)
@HerringtonDarkholme Thanks for suggestion)
Any advice is it possible to solve more complicated case, to check if target
object, contains props of orignal
object with the same corresponding types?
function compareTypesOfKeys<
KeyS extends string, KeyN extends string>
(original: { [K in (Keys & KeyN)]: any}):
(target: {[K in KeyS]: string} & {[K in KeyN]: number}) => boolean // this doesn't work
{
return (target: any): boolean => {
let isTheSameTypesOfKeys: boolean = true
Object.keys(original).forEach((keyInOriginal) => {
if (typeof target[keyInOriginal] !== typeof original[keyInOriginal]) {
isTheSameTypesOfKeys = false
}
})
return isTheSameTypesOfKeys
}
}
compareTypesOfKeys({
str: 'str',
num: 1
})({ str: 'stringValue', num: 1 })
@mhegazy: I wouldn't consider this properly resolved; the known workaround of mass overloading gives performance issues to the extent of no longer being able to compile. This is in need for a better solution than is possible today.
I have another work around, tested on typescript 2.3.4:
Still can't type array based paths such as the one of ramda
Given the function:
/**
* Create a deep path builder for a given type
*/
export function path<T>() {
/**Returns a function that gets the next path builder */
function subpath<T, TKey extends keyof T>(parent: string[], key: TKey): PathResult<T[TKey]> {
const newPath = [...parent, key];
const x = (<TSubKey extends keyof T[TKey]>(subkey: TSubKey) => subpath<T[TKey], TSubKey>(newPath, subkey)) as PathResult<T[TKey]>;
x.path = newPath;
return x;
}
return <TKey extends keyof T>(key: TKey) => subpath<T, TKey>([], key);
}
Use:
interface MyDeepType {
person: {
names: {
lastnames: {
first: string,
second: string;
}
firstname: string;
}
age: number;
}
other: string;
}
//All path parts are checked and intellisense enabled:
const x = path<MyDeepType>()("person")("names")("lastnames")("second");
const myPath: string[] = x.path;
So close, yet so far:
export type Inc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256];
export type TupleHasIndex<Arr extends any[], I extends number> = ({[K in keyof Arr]: '1' } & Array<'0'>)[I];
// ^ #15768, TS2536 `X cannot be used to index Y` on generic. still works though.
export type PathFn<T, R extends Array<string | number>, I extends number = 0> =
{ 1: PathFn<T[R[I]], R, Inc[I]>, 0: T }[TupleHasIndex<R, I>];
type PathTest = PathFn<{ a: { b: ['c', { d: 'e' }] } }, ['a', 'b', 1, 'd']>;
// "e". yay!
export declare function path<T, R extends Array<string|number>>(obj: T, path: R): PathFn<T, R>;
const pathTest = path({ a: { b: ['c', { d: 'e' }] } }, ['a', 'b', 1, 'd'])
// { a: { b: (string | { d: string; })[]; }; }. weird...
Edit: filed what I believe to be a minimum repro of this issue at #17086.
cc @ikatyang
I got a working function-based PoC for path
on master now:
type Inc = { [k: string]: string; 0:'1', 1:'2', 2:'3', 3:'4', 4:'5', 5:'6', 6:'7', 7:'8', 8:'9' };
type StringToNumber = { [k: string]: number; 0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8};
type TupleHasIndex<Arr extends any[], I extends string> = ({[K in keyof Arr]: '1' } & { [k: string]: '0' })[I];
type PathFn<T extends { [k: string]: any }, R extends Array<string>, I extends string = '0'> =
{ 1: PathFn<T[R[StringToNumber[I]]], R, Inc[I]>, 0: T }[TupleHasIndex<R, I>];
type PathTest = PathFn<{ a: { b: ['c', { d: 'e' }] } }, ['a', 'b', '1', 'd']>;
// "e"
declare function path<T extends { [k: string]: any }, R extends Array<string>>(obj: T, path: R): PathFn<T, R>;
let obj: { a: { b: ['c', { d: 'e' }] } };
let keys: ['a', 'b', '1', 'd'];
const pathTest = path(obj, keys);
// "e"
So the numbers are passed as keys there and the outer structure here must have a string index (non-array), but yay, progress!
@tycho01
Good news: it also works with v2.5.1. ๐
Bad news: needs to provide exact type (no widen) and contextual inference seems not working here, but yeah, progress! ๐
declare const obj: { a: { b: ['c', { d: 'e' }] } };
declare const keys: ['a', 'b', '1', 'd'];
path(obj, keys) //=> ๐
path(obj, ['a', 'b', '1', 'd']) //=> ๐ฅ
path({ a: { b: ['c', { d: 'e' }] } }, keys) //=> ๐ฅ
path({ a: { b: ['c', { d: 'e' }] } }, ['a', 'b', '1', 'd']) //=> ๐ฅ
@ikatyang: Yeah. At least the widening issue I'm hoping to resolve with #17785.
I might be a little late on the issue here but, since v2.8 released, with the infer
keyword and conditional types I was able to cook up this:
type KeyOf<T> = keyof T;
interface DeepKeyOfArray<T> extends Array<string> {
['0']?: KeyOf<T>;
['1']?: this extends {
['0']?: infer K0
} ? K0 extends KeyOf<T> ? KeyOf<T[K0]> : never : never;
['2']?: this extends {
['0']?: infer K0;
['1']?: infer K1;
} ? K0 extends KeyOf<T> ? K1 extends KeyOf<T[K0]> ? KeyOf<T[K0][K1]> : never : never : never;
['3']?: this extends {
['0']?: infer K0;
['1']?: infer K1;
['2']?: infer K2;
} ? K0 extends KeyOf<T> ? K1 extends KeyOf<T[K0]> ? K2 extends KeyOf<T[K0][K1]> ? KeyOf<T[K0][K1][K2]> : never : never : never : never;
['4']?: this extends {
['0']?: infer K0;
['1']?: infer K1;
['2']?: infer K2;
['3']?: infer K3;
} ? K0 extends KeyOf<T> ? K1 extends KeyOf<T[K0]> ? K2 extends KeyOf<T[K0][K1]> ? K3 extends KeyOf<T[K0][K1][K2]> ? KeyOf<T[K0][K1][K2][K3]> : never : never : never : never : never;
['5']?: this extends {
['0']?: infer K0;
['1']?: infer K1;
['2']?: infer K2;
['3']?: infer K3;
['4']?: infer K4;
} ? K0 extends KeyOf<T> ? K1 extends KeyOf<T[K0]> ? K2 extends KeyOf<T[K0][K1]> ? K3 extends KeyOf<T[K0][K1][K2]> ? K4 extends KeyOf<T[K0][K1][K2][K3]> ? KeyOf<T[K0][K1][K2][K3][K4]> : never : never : never : never : never : never;
['6']?: this extends {
['0']?: infer K0;
['1']?: infer K1;
['2']?: infer K2;
['3']?: infer K3;
['4']?: infer K4;
['5']?: infer K5;
} ? K0 extends KeyOf<T> ? K1 extends KeyOf<T[K0]> ? K2 extends KeyOf<T[K0][K1]> ? K3 extends KeyOf<T[K0][K1][K2]> ? K4 extends KeyOf<T[K0][K1][K2][K3]> ? K5 extends KeyOf<T[K0][K1][K2][K3][K4]> ? KeyOf<T[K0][K1][K2][K3][K4][K5]> : never : never : never : never : never : never : never;
}
type ArrayHasIndex<MinLenght extends string> = { [K in MinLenght]: any; };
export type DeepTypeOfArray<T, L extends DeepKeyOfArray<T>> =
L extends ArrayHasIndex<'7'> ?
any :
L extends ArrayHasIndex<'6'> ?
T[L['0']][L['1']][L['2']][L['3']][L['4']][L['5']][L['6']] :
L extends ArrayHasIndex<'5'> ?
T[L['0']][L['1']][L['2']][L['3']][L['4']][L['5']] :
L extends ArrayHasIndex<'4'> ?
T[L['0']][L['1']][L['2']][L['3']][L['4']] :
L extends ArrayHasIndex<'3'> ?
T[L['0']][L['1']][L['2']][L['3']] :
L extends ArrayHasIndex<'2'> ?
T[L['0']][L['1']][L['2']] :
L extends ArrayHasIndex<'1'> ?
T[L['0']][L['1']] :
L extends ArrayHasIndex<'0'> ?
T[L['0']] :
T;
export type DeepKeyOf<T> = DeepKeyOfArray<T> | KeyOf<T>;
export type DeepTypeOf<T, L extends DeepKeyOf<T>> =
L extends DeepKeyOfArray<T> ?
DeepTypeOfArray<T, L> :
L extends KeyOf<T> ?
T[L] :
never;
This is really verbose but allows the input array to be checked with keyof recursively.
declare function path<T, L extends DeepKeyOf<T>>(object: T, params?: L): DeepTypeOf<T, L>;
const obj = {
v: { w: { x: { y: { z: { a: { b: { c: 2 } } } } } } }
};
const output: number = path(obj, ['v', 'w', 'x']); // ๐ฅ
const output2: object = path(obj, ['v', 'w', 'x']); // โ๏ธ
const output4: { c: string } = path(obj, ['v', 'w', 'x', 'y', 'z', 'a', 'b']); // ๐ฅ
const output3: { c: number } = path(obj, ['v', 'w', 'x', 'y', 'z', 'a', 'b']); // โ๏ธ
const output5: { wrong: 'type' } = path(obj, ['v', 'w', 'x', 'y', 'z', 'a', 'b', 'c']); // โ๏ธ since after 7 levels there is no typechecking
path(obj, '!'); // ๐ฅ
path(obj, ['!']); // ๐ฅ
path(obj, ['v', '!']); // ๐ฅ
path(obj, ['v', 'w', '!']); // ๐ฅ
path(obj, ['v', 'w', 'x', '!']); // ๐ฅ
path(obj, ['v', 'w', 'x', 'y', '!']); // ๐ฅ
path(obj, ['v', 'w', 'x', 'y', 'z', '!']); // ๐ฅ
path(obj, ['v', 'w', 'x', 'y', 'z', 'a', '!']); // ๐ฅ
path(obj, ['v', 'w', 'x', 'y', 'z', 'a', 'b', '!']); // โ๏ธ since after 7 levels there is no typechecking
path(obj, 'v'); // โ๏ธ
path(obj, ['v']); // โ๏ธ
path(obj, ['v', 'w']); // โ๏ธ
path(obj, ['v', 'w', 'x']); // โ๏ธ
path(obj, ['v', 'w', 'x', 'y']); // โ๏ธ
path(obj, ['v', 'w', 'x', 'y', 'z']); // โ๏ธ
path(obj, ['v', 'w', 'x', 'y', 'z', 'a']); // โ๏ธ
path(obj, ['v', 'w', 'x', 'y', 'z', 'a', 'b']); // โ๏ธ
path(obj, ['v', 'w', 'x', 'y', 'z', 'a', 'b', 'c']); // โ๏ธ
@Goobles Impressive work! This is clearly an advancement. I have a question though: do you get compilation errors inside DeepTypeOfArray
? It seems TypeScript doesn't like L['0']
used to index T
.
Refer to TypeScript playground to see the errors.
I haven't found a way to restrict inferred types to their respective branches, too.
Please consider a following example:
const obj = {
v: {
w: { x: { y: { z: { a: { b: { c: 2 } } } } } },
ouch: true, // Additional property at level 2
}
};
A property called ouch
was introduced next to obj.v.w
. It's now possible to take valid path used to retrieve output3
, swap the property names w
โ ouch
and continue without an error, even though the path is no longer correct.
path(obj, ['v', 'ouch', 'x', 'y', 'z', 'a', 'b']) === output3; // No error (wrong)
Is it even possible, in your opinion, to cover such cases?
@karol-majewski looks like L['0']
yields T | undefined
as we haven't proven (to TS) that T
contains 0
.
Now, the easiest way to prove this would seem to be to check not just for the highest index, but for all of them, meaning we'd need something more complex than just ArrayHasIndex
.
I guess that could be done with a switch
-like extends
chain as you have been doing, or maybe using recursion as well, (though I liked to try and use that to handle the other logic here as well).
@karol-majewski apparently L needs to be a type that has all the keys, not just the last one
I fixed the playground example to not have that error:
also, the w
-> ouch
problem is because the Boolean and Number type somehow has a property named x:any
, in the compiler. I am not sure why.
If you try
path(obj, ['v','ouch','anythingElse'])
it would give a compilation error (I also added this as an example in the playground)
Nevermind it's because w
has the property x
and DeepKeyOf takes the union of w
and ouch
Would love to see something like this for Lodash's get
for the path.
@luchillo17 fwiw, TS can't handle notations like _.get(object, 'a[0].b.c')
-- can't split string literal types on .
. Nor do other operations on type literals, see e.g. #15645 for numbers.
I know, if it could someone would have changed the typings by now, i'm just saying this thread relates a lot to that feature in lodash.
@luchillo17 Yeah :), all but that bit should apply there too.
@Goobles @karol-majewski are you not getting this type error?
Type 'L["0"]' cannot be used to index type 'T'.
(type parameter) T in type DeepTypeOfArray<T, L extends DeepKeyOfArray<T>>
@ccorcos Not in the one fixed by @Goobles. ;-)
I see. Thanks @karol-majewski
Here's the result of me playing around with this.
interface Record {
a: number
b?: number
c: Array<string>
d?: Array<string>
e: {
a: number
b?: number
},
f?: {
a: number
b?: number
},
}
declare const record: Record
path(record, ["a"]) // โ๏ธ
path(record, ["b"]) // โ๏ธ
path(record, ["c"]) // โ๏ธ
path(record, ["c", 0]) // ๐ฅ
path(record, ["e", "a"]) // โ๏ธ
path(record, ["f", "a"]) // โ๏ธ except the type is any ๐ฅ
I'm having trouble with objects that have optional types in their path...
Its also not possible to index into an array...
@ccorcos You can probably handle optional types by applying DeepFull
in `path:
declare function path<T, L extends DeepKeyOf<DeepFull<T>>>(object: T, params?: L): DeepTypeOf<DeepFull<T>, L>;
In case it's of interest to anyone, a while ago I used @Goobles's solution to build some extensible lens building utilities using proxies. Unfortunately, upgrading to 2.9 makes type checking it unusably slow, and I'm guessing that's the case for anything using this solution of sufficient size - might have just been my particular setup though.
With ts-pathof
import { pathOf } from 'ts-pathof';
const obj = {a: {b: 2}};
R.path(pathOf(obj, 'a', 'b'), obj);
@Morglod having a look at the source it should work for most of the cases but could break on some weirdly deep cases
@agalazis just updated ts-pathof
Now you there is hasPath
:
import { hasPath } from 'ts-pathof';
const c = { z: { y: { bb: 123 }}};
const path = hasPath(c, [ 'z', 'y', 'bb' ]);
path -> [ 'z', 'y', 'bb' ]
const path2 = hasPath(c, [ 'z', 'y', 'gg' ]); // no error
path2 -> value is false, type is never
So you can:
import { hasPath} from 'ts-pathof';
const obj = {a: {b: 2}};
R.path(hasPath(obj, ['a', 'b']), obj);
For some reason @Goobles when I copy your solution to work on it in my editor I see the following TypeScript error.
['X'] is referenced directly or indirectly in its own type annotation
Not sure what's causing the issue. I'm using TS 3.1.3
@ifiokjr try replace ?: this
everywhere... exactly only "this" -> "DeepKeyOfArray
@Morglod @agalazis I've implemented a 'recursive' version of PathOf:
interface NextInt {
0: 1,
1: 2,
2: 3,
3: 4,
4: 5
[rest: number]: number
}
type PathType<T, P extends string[], Index extends (keyof P & number) = 0> = {
[K in (keyof P & number & Index)]: P[K] extends undefined
? T
: P[K] extends keyof T
? NextInt[K] extends (keyof P & number)
? PathType<T[P[K]], P, Extract<NextInt[K], (keyof P & number)>>
: T[P[K]]
: never
}[Index];
The only non-dynamic part is NextInt
which, as above, only supports path lengths of up to 5. The mapped type business was required to get around the cyclical reference restrictions.
Usage:
const o = {x: { y: 10 }};
type x = PathType<typeof o, []>; // => typeof o
type xy = PathType<typeof o, ['x', 'y']>; // => number
type xyz = PathType<typeof o, ['x', 'y', 'z']>; // => never
@irond13 Thats the main problem with all recursive implementations - restrictions in TS for cyclical references ๐
@irond13 Thats the main problem with all recursive implementations - restrictions in TS for cyclical references ๐
In this case, it's more of an annoyance than a blocker.
FYI, to support longer path lengths, the NextInt
interface just needs to be expanded. So, with the following, path lengths of up to 10 are supported:
interface NextInt {
0: 1,
1: 2,
2: 3,
3: 4,
4: 5,
5: 6,
6: 7,
7: 8,
8: 9,
9: 10
[rest: number]: number
}
I wonder how long it'll take us to pull off lenses/traversals/prisms. :D
Just playing around with some edge cases:
type X = {
a: 1,
b: { c: 2 } | { d: 3 }
c?: Array<{ d: string }>
d?: {
e?: {
f: 1
}
}
}
// Doesn't work with union types.
type A = PathType<X, ["b", "c"]>
// Doesn't work with Arrays
type B = PathType<X, ["c", "d"]>
// Doesn't work with optional types with strictNullChecks
type C = PathType<X, ["d", "e"]>
I tweaked a little @Goobles' code so as to make it work with the latest versions of TypeScript:
interface PathArray<T, L> extends Array<string | number> {
["0"]?: keyof T;
["1"]?: L extends {
["0"]: infer K0;
}
? K0 extends keyof T
? keyof T[K0]
: never
: never;
["2"]?: L extends {
["0"]: infer K0;
["1"]: infer K1;
}
? K0 extends keyof T
? K1 extends keyof T[K0]
? keyof T[K0][K1]
: never
: never
: never;
["3"]?: L extends {
["0"]: infer K0;
["1"]: infer K1;
["2"]: infer K2;
}
? K0 extends keyof T
? K1 extends keyof T[K0]
? K2 extends keyof T[K0][K1]
? keyof T[K0][K1][K2]
: never
: never
: never
: never;
["4"]?: L extends {
["0"]: infer K0;
["1"]: infer K1;
["2"]: infer K2;
["3"]: infer K3;
}
? K0 extends keyof T
? K1 extends keyof T[K0]
? K2 extends keyof T[K0][K1]
? K3 extends keyof T[K0][K1][K2]
? keyof T[K0][K1][K2][K3]
: never
: never
: never
: never
: never;
["5"]?: L extends {
["0"]: infer K0;
["1"]: infer K1;
["2"]: infer K2;
["3"]: infer K3;
["4"]: infer K4;
}
? K0 extends keyof T
? K1 extends keyof T[K0]
? K2 extends keyof T[K0][K1]
? K3 extends keyof T[K0][K1][K2]
? K4 extends keyof T[K0][K1][K2][K3]
? keyof T[K0][K1][K2][K3][K4]
: never
: never
: never
: never
: never
: never;
["6"]?: L extends {
["0"]: infer K0;
["1"]: infer K1;
["2"]: infer K2;
["3"]: infer K3;
["4"]: infer K4;
["5"]: infer K5;
}
? K0 extends keyof T
? K1 extends keyof T[K0]
? K2 extends keyof T[K0][K1]
? K3 extends keyof T[K0][K1][K2]
? K4 extends keyof T[K0][K1][K2][K3]
? K5 extends keyof T[K0][K1][K2][K3][K4]
? keyof T[K0][K1][K2][K3][K4][K5]
: never
: never
: never
: never
: never
: never
: never;
}
type ArrayHasIndex<MinLenght extends number> = { [K in MinLenght]: any };
export type PathArrayValue<
T,
L extends PathArray<T, L>
> = L extends ArrayHasIndex<0 | 1 | 2 | 3 | 4 | 5 | 6 | 7>
? any
: L extends ArrayHasIndex<0 | 1 | 2 | 3 | 4 | 5 | 6>
? T[L[0]][L[1]][L[2]][L[3]][L[4]][L[5]][L[6]]
: L extends ArrayHasIndex<0 | 1 | 2 | 3 | 4 | 5>
? T[L[0]][L[1]][L[2]][L[3]][L[4]][L[5]]
: L extends ArrayHasIndex<0 | 1 | 2 | 3 | 4>
? T[L[0]][L[1]][L[2]][L[3]][L[4]]
: L extends ArrayHasIndex<0 | 1 | 2 | 3>
? T[L[0]][L[1]][L[2]][L[3]]
: L extends ArrayHasIndex<0 | 1 | 2>
? T[L[0]][L[1]][L[2]]
: L extends ArrayHasIndex<0 | 1>
? T[L[0]][L[1]]
: L extends ArrayHasIndex<0>
? T[L[0]]
: never;
export type Path<T, L> = PathArray<T, L> | keyof T;
export type PathValue<T, L extends Path<T, L>> = L extends PathArray<T, L>
? PathArrayValue<T, L>
: L extends keyof T
? T[L]
: any;
declare function path<T, L extends Path<T, L>>(
object: T,
params: L
): PathValue<T, L>;
const obj = {
v: {
w: { x: { y: { z: { a: { b: { c: 2 } } } } } },
ouch: true,
foo: [{ bar: 2 }, { bar: 3 }]
}
};
const output: number = path(obj, ["v", "w", "x"]); // ๐ฅ
const output2: object = path(obj, ["v", "w", "x"]); // โ๏ธ
const output4: { c: string } = path(obj, ["v", "w", "x", "y", "z", "a", "b"]); // ๐ฅ
const output3: { c: number } = path(obj, ["v", "w", "x", "y", "z", "a", "b"]); // โ๏ธ
const output5: "wrong" = path(obj, ["v", "w", "x", "y", "z", "a", "b", "c"]); // โ๏ธ since after 7 levels there is no typechecking
const x = path(obj, ["v", "ouch", "x"]); // ๐ฅ
const y = path(obj, ["v", "ouch", "y"]); // ๐ฅ
const z = path(obj, ["v", "ouch", "somethingCompletelyDifferent"]); // ๐ฅ
path(obj, "!"); // ๐ฅ
path(obj, ["!"]); // ๐ฅ
path(obj, ["v", "!"]); // ๐ฅ
path(obj, ["v", "w", "!"]); // ๐ฅ
path(obj, ["v", "w", "x", "!"]); // ๐ฅ
path(obj, ["v", "w", "x", "y", "!"]); // ๐ฅ
path(obj, ["v", "w", "x", "y", "z", "!"]); // ๐ฅ
path(obj, ["v", "w", "x", "y", "z", "a", "!"]); // ๐ฅ
path(obj, ["v", "w", "x", "y", "z", "a", "b", "!"]); // โ๏ธ since after 7 levels there is no typechecking
path(obj, "v"); // โ๏ธ
path(obj, ["v"]); // โ๏ธ
path(obj, ["v", "w"]); // โ๏ธ
path(obj, ["v", "w", "x"]); // โ๏ธ
path(obj, ["v", "w", "x", "y"]); // โ๏ธ
path(obj, ["v", "w", "x", "y", "z"]); // โ๏ธ
path(obj, ["v", "w", "x", "y", "z", "a"]); // โ๏ธ
path(obj, ["v", "w", "x", "y", "z", "a", "b"]); // โ๏ธ
path(obj, ["v", "w", "x", "y", "z", "a", "b", "c"]); // โ๏ธ
Just playing around with some edge cases:
type X = { a: 1, b: { c: 2 } | { d: 3 } c?: Array<{ d: string }> d?: { e?: { f: 1 } } } // Doesn't work with union types. type A = PathType<X, ["b", "c"]> // Doesn't work with Arrays type B = PathType<X, ["c", "d"]> // Doesn't work with optional types with strictNullChecks type C = PathType<X, ["d", "e"]>
This does work with arrays though, you have to specify the array index in the property traversal array (oh and change the constraints for P
to P extends (string|number)[]
so it allows number
as well)
type B = PathType<X, ["c", 0, "d"]>
Hello all here is my proposal, something along the lines of pathof as new functionality or being able to spread keyof recursively would be nice to have:
https://github.com/Microsoft/TypeScript/issues/20423
I've try solve this typings.
I used this recursive type definition method. (https://github.com/Microsoft/TypeScript/issues/14833)
I hope that will help someone.
type Head<U> = U extends [any, ...any[]]
? ((...args: U) => any) extends (head: infer H, ...args: any) => any
? H
: never
: never;
type Tail<U> = U extends [any, any, ...any[]]
? ((...args: U) => any) extends (head: any, ...args: infer T) => any
? T
: never
: never;
type TraversePath<State extends any, T extends any[]> = Head<T> extends keyof State
? {
0: State[Head<T>];
1: TraversePath<State[Head<T>], Tail<T>>;
}[Tail<T> extends never ? 0 : 1]
: never;
export function get<State extends any, Paths extends (string | number)[]>(state: State, ...paths: Paths): TraversePath<State, Paths> {
const [head, ...tail] = paths;
if (!state.hasOwnProperty(head)) {
throw new Error(`state has not ${head}`);
}
if (tail.length) {
return get(state[head], ...tail);
}
return state[head];
}
const obj = {
v: {
w: { x: { y: { z: { a: { b: { c: 2 } } } } } },
ouch: true,
foo: [{ bar: 2 }, { bar: 3 }]
}
};
const a: typeof obj['v']['w']['x'] = get(obj, 'v', 'w', 'x'); // โ
const b: typeof obj['v']['ouch'] = get(obj, 'v', 'w', 'x'); // ๐ฅ
@nanot1m and I wrote small, but perfectly working solution. I hope it will help to you too.
First of all, let's talk about function getIn takes 3 arguments: searchable โ object, path โ array of strings/variables, default value.
It tries to get value of the target property in searchable object, if it fails returns default value.
That typings of getIn tries to compute type of the target property when it exists. If it was an optional field, it makes it required.
Returns union of that type and type of default value, 'cos in some cases they are different.
It works perfectly for me, but the realisation has some limitations.
Here they are:
For example, []
โ has type never[]
or {}
โ has type {}
. And I hope you expect something else.
Solution 1. Definition of variable with certain type:
const defaultValue: string[] = [];
const a = { b: ['test'] };
const stringValues = getIn(a, ['b'], defaultValue);
Solution 2 (Unpreferable). Don't pass default value:
const a = { b: ['test'] };
const stringValues = getIn(a, ['b'], defaultValue) || [];
string[]
type you should override type to [string]const path = getPath('awesome'); // string[]
const data = getIn(a, path as [string], 'default value'); // string[] -> [string]
string
it's not a property name.const a = { b: { c: 12 } };
const pathPart = 'c' as 'c';
const data = getIn(a, ['a, 'b', pathPart]);
Solution:
type Diff<T, U> = T extends U ? never : T;
type FilterProps<O, P, D> = P extends keyof Diff<O, number | string | null | undefined> ? (Required<Diff<O, number | string | null | undefined>>[P] | D) : D;
declare function getIn<O extends {}, P1 extends string, D = null>(obj: O, path: [P1], dafaultValue?: D): FilterProps<O, P1, D>;
declare function getIn<O extends {}, P1 extends string, P2 extends string, D = null>(obj: O, path: [P1, P2], defaultValue?: D): FilterProps<FilterProps<O, P1, D>, P2, D>;
declare function getIn<O extends {}, P1 extends string, P2 extends string, P3 extends string, D = null>(obj: O, path: [P1, P2, P3], defaultValue?: D): FilterProps<FilterProps<FilterProps<O, P1, D>, P2, D>, P3, D>;
declare function getIn<O extends {}, P1 extends string, P2 extends string, P3 extends string, P4 extends string, D = null>(obj: O, path: [P1, P2, P3, P4], defaultValue?: D): FilterProps<FilterProps<FilterProps<FilterProps<O, P1, D>, P2, D>, P3, D>, P4, D>;
declare function getIn<O extends {}, P1 extends string, P2 extends string, P3 extends string, P4 extends string, P5 extends string, D = null>(obj: O, path: [P1, P2, P3, P4, P5], defaultValue?: D): FilterProps<FilterProps<FilterProps<FilterProps<FilterProps<O, P1, D>, P2, D>, P3, D>, P4, D>, P5, D>;
declare var k: { a: { a: { user: { login: string; } } } };
var a = getIn(k, ['a', 'a', 'user', 'login']);
var b = getIn({ a: { b: 1 } }, ['a', 'b'], '42')
var c = getIn({ a: { b: { c: 2 } } }, ['a', 'b', 'c'], '42')
I adapted @irond13's PathType to handle strictNullChecks and optional types!
interface NextInt {
0: 1
1: 2
2: 3
3: 4
4: 5
[rest: number]: number
}
// prettier-ignore
type PathType<Obj, Path extends Array<string | number>, Index extends number = 0> = {
// Need to use this object indexing pattern to avoid circular reference error.
[Key in Index]: Path[Key] extends undefined
// Return Obj when we reach the end of the Path.
? Obj
// Check if the Key is in the Obj.
: Path[Key] extends keyof Obj
// If the Value does not contain null.
// `T & {}` is a trick to remove undefined from a union type.
? Obj[Path[Key]] extends Obj[Path[Key]] & {}
? PathType<
Obj[Path[Key]],
Path,
Extract<NextInt[Key], number>
>
// Remove the undefined from the Value, and add it to the union after.
: undefined | PathType<
Obj[Path[Key]] & {},
Path,
Extract<NextInt[Key], number>
>
: never
}[Index]
type Test = {
a: 1
b: { c: 2 } | { d: 3 }
c?: Array<{ d: string }>
d?: {
e?: {
f: 1
}
}
g: {
h: 10
}
}
type Assert<T, V extends T> = V
type _a = PathType<Test, []>
type a = Assert<_a, Test>
type _b = PathType<Test, ["b"]>
type b = Assert<_b, Test["b"]>
type _c0 = PathType<Test, ["c", 0]>
type c0 = Assert<_c0, { d: string } | undefined>
type _c0d = PathType<Test, ["c", 0, "d"]>
type c0d = Assert<_c0d, string | undefined>
type _de = PathType<Test, ["d", "e"]>
type de = Assert<_de, { f: 1 } | undefined>
type _def = PathType<Test, ["d", "e", "f"]>
type def = Assert<_def, 1 | undefined>
type _g = PathType<Test, ["g"]>
type g = Assert<_g, {h: 10}>
type _gh = PathType<Test, ["g", "h"]>
type gh = Assert<_gh, 10>
type _ghz = PathType<Test, ["g", "h", "z"]>
type ghz = Assert<_ghz, never>
Sadly, I just realized this isn't the type that I need! ๐ญ
I want a type that enforces a valid path.
function get<O, P extends PathOf<O>>(o: O, p: P): PathType<O, P> {}
Otherwise I get Type instantiation is excessively deep and possibly infinite.
Example of this error "Type instantiation is excessively deep and possibly infinite."
function getPath<O, P extends Array<number | string>>(
o: O,
p: P
): PathType<O, P> {
return {} as any
}
This is rather frustrating. I really thought there would be a way to do this, but seems like Typescript isn't ready yet.
I've been trying out your examples @ccorcos , the one earlier seemed to work quite well until I discovered that the inferred type for the deep key value is not being used properly in lower levels:
interface DeepKeyOfArray<O> extends Array<string | number> {
["0"]: TKeyOf<O>;
["1"]?: this extends {
["0"]: infer K0;
}
? K0 extends TKeyOf<O> ? TKeyOf<O[K0]> : never : never;
["2"]?: this extends {
["0"]: infer K0;
["1"]: infer K1;
}
? K0 extends TKeyOf<O>
? (K1 extends TKeyOf<O[K0]> ? TKeyOf<O[K0][K1]> : never)
: never
: never;
["3"]?: this extends {
["0"]: infer K0;
["1"]: infer K1;
["2"]: infer K2;
}
? K0 extends TKeyOf<O>
? K1 extends TKeyOf<O[K0]>
? K2 extends TKeyOf<O[K0][K1]>
? TKeyOf<O[K0][K1][K2]> : never : never : never : never;
}
interface IObj {
count: boolean;
tagsFirst: string[];
deep: {
color: string;
tags: string[];
deeper: {
egg: boolean;
more: {
other: number;
};
};
};
}
const path: DeepKeyOfArray<IObj> = ["count", "deeper"];
That path
at the bottom there doesn't throw any errors when its clear to see that count
is as deep as it goes here, but it continues to allow any keys that are at the second level.
Path referencing in JSON and JavaScript programming is quite a common use case... Would be really nice if we could have an easier way to deal with this.
@ccorcos I've found a workaround to that error although the function signature becomes artificially more complicated
function getPath<O, P extends ReadonlyArray<number | string>>(
o: O,
p: P
): () => PathType<O, P> {
return {} as any
}
also, I've replaced Array
So any way typescript support Dot Notation that's also problematic with things like MongoDB
While I liked the above functions, I thought the logic is a bit hard to follow. Here's a more verbose, but easy-to-read version that fully typechecks paths of <7 length:
export type PathOfLength1<T, K1 extends keyof T> = [K1];
export type PathOfLength2<T, K1 extends keyof T, K2 extends keyof T[K1]> = [
K1,
K2,
];
export type PathOfLength3<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2]
> = [K1, K2, K3];
export type PathOfLength4<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3]
> = [K1, K2, K3, K4];
export type PathOfLength5<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4]
> = [K1, K2, K3, K4, K5];
export type PathOfLength6<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5]
> = [K1, K2, K3, K4, K5, K6];
export type PathOfLength7Plus<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5],
K7 extends keyof T[K1][K2][K3][K4][K5][K6]
> = [K1, K2, K3, K4, K5, K6, K7, ...(number | string)[]];
/**
* A function to validate that a path is valid for a variable of type T.
* It simply returns the path itself, and doesn't do anything. The main value
* is that it infers the types via function overloading.
*
* The best way to use this is to not explicitly state any types in the
* generics and to pass a dummy value of the type the path should apply to
* and a path.
*
* If the path is invalid, it won't typecheck. Unfortunately, the autocomplete
* for `path` doens't work super well though.
*/
export function validatePath<T, K1 extends keyof T>(
_dummyValue: T,
path: PathOfLength1<T, K1>,
): PathOfLength1<T, K1>;
export function validatePath<T, K1 extends keyof T, K2 extends keyof T[K1]>(
_dummyValue: T,
path: PathOfLength2<T, K1, K2>,
): PathOfLength2<T, K1, K2>;
export function validatePath<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2]
>(
_dummyValue: T,
path: PathOfLength3<T, K1, K2, K3>,
): PathOfLength3<T, K1, K2, K3>;
export function validatePath<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3]
>(
_dummyValue: T,
path: PathOfLength4<T, K1, K2, K3, K4>,
): PathOfLength4<T, K1, K2, K3, K4>;
export function validatePath<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4]
>(
_dummyValue: T,
path: PathOfLength5<T, K1, K2, K3, K4, K5>,
): PathOfLength5<T, K1, K2, K3, K4, K5>;
export function validatePath<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5]
>(
_dummyValue: T,
path: PathOfLength6<T, K1, K2, K3, K4, K5, K6>,
): PathOfLength6<T, K1, K2, K3, K4, K5, K6>;
export function validatePath<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5],
K7 extends keyof T[K1][K2][K3][K4][K5][K6]
>(
_dummyValue: T,
path: PathOfLength7Plus<T, K1, K2, K3, K4, K5, K6, K7>,
): PathOfLength7Plus<T, K1, K2, K3, K4, K5, K6, K7>;
export function validatePath<T>(_dummyValue: T, path: unknown): unknown {
return path;
}
This looks nice: https://github.com/bsalex/typed-path/
The best part about it is that it allows using with any pre-existing methods that accept property paths.
But the syntax is so verbose and hard to understand that it defeats the purpose.
Instead of
foo.mapBy('a.b.c')
I'd rather do
foo.map(e => e.a.b.c)
than
foo.mapBy(tp<Foo>().a.b.c.toString())
Interesting idea indeed!
On Fri, 24 Apr 2020, 12:30 Andrey Mikhaylov (lolmaus), <
[email protected]> wrote:
This looks nice: https://github.com/bsalex/typed-path/
The best part about it is that it allows using with any pre-existing
methods that accept property paths.But the syntax is so verbose and hard to understand that it defeats the
purpose.Instead of
foo.mapBy('a.b.c')
I'd rather do
foo.map(e => e.a.b.c)
than
foo.mapBy(tp
().a.b.c.toString()) โ
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/microsoft/TypeScript/issues/12290#issuecomment-618907152,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAPNSTMF7SC4AR4EMFCWFHTROFL3VANCNFSM4CWNS4SQ
.
Thanks all for the suggestions. They're unfortunately all so hacky and verbose, and sometimes unusable in certain situations. I've tried implementing them, and its as if one day they work and the next day they don't (maybe TypeScript version changes etc.). They also very complex and super difficult to debug.
I really just wish the TypeScript team would add an ObjectKeyPath<T>
utility function, which is an array of variable length but each item conforms to the deep key path of object T
.
This kind of functionality is so important in JavaScript and JSON, especially for JSON Patch kind of functionality.
immer
, one of my favorite JavaScript libraries, implements "patches", a kind of diff between object operations, which makes use of object deep paths to show which parts have changed. I'd really like to take advantage of this functionality in my own libraries and have it nicely typed for the user.
There are _many_ good uses for such functionality. Big one being smart undo / redo of changes to an object structure.
I wish the TypeScript team would care a lil more about this type. It seems to be quite a non-prioritized thing sadly...
I totally much agree. Seems like this could be implemented internally with much better success. Any thoughts? @mhegazy @RyanCavanaugh
FWIW, ts-toolbelt's Object.Path type has been very effective at properly typing most non-trivial use cases I've thrown at it:
import { O } from 'ts-toolbelt'
type T = {
a: {
b: { c: number } | { c: string },
d: { e: boolean }[]
}
}
type C = O.Path<T, ['a', 'b', 'c']> // type C = string | number
type E = O.Path<T, ['a', 'd', 0, 'e']> // type E = boolean
type F = O.Path<T, ['a', 'b', 'c', 'f']> // type F = never
type G = O.Path<T, ['g']> // type G = never
An approach to typing a path function (with the unfortunate caveat that the function is variadic, rather than taking an array as the second argument) could look like:
declare const path: <T extends object, P extends (string | number)[]>(value: T, ...path: P) => O.Path<T, P>
declare const t: T
const c = path(t, 'a', 'b', 'c') // c: string | number
Not a complete solution, but it's been more reliable than other approaches I've taken.
Most helpful comment
You can already do that with manual overloading.