-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Typing for object deep paths #12290
Comments
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. 😄 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 |
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:
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. |
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 (-> |
@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
|
@whitecolor: what if you make that On that |
In 10 years maybe =) |
The problem here is how TypeScript infer type argument. The In such condition, One solution is using curry to help compiler infer type argument. |
@HerringtonDarkholme Thanks for suggestion) Any advice is it possible to solve more complicated case, to check if 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: /**
* 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. |
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 |
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 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<number | string> with ReadonlyArray<number | string> for the sake of type inference |
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
I'd rather do
than
|
Interesting idea indeed!
…On Fri, 24 Apr 2020, 12:30 Andrey Mikhaylov (lolmaus), < ***@***.***> 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<Foo>().a.b.c.toString())
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#12290 (comment)>,
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 This kind of functionality is so important in JavaScript and JSON, especially for JSON Patch kind of functionality.
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. |
Guys, I think I made it work with the new recursive types type ExtractObj<S extends object, K> = K extends keyof S ? S[K] : never
type Path<S extends object, T extends readonly unknown[]> =
T extends readonly [infer T0, ...infer TR]
? TR extends []
? ExtractObj<S, T0> extends never
? readonly []
: readonly [T0]
: ExtractObj<S, T0> extends object
? readonly [T0, ...Path<ExtractObj<S, T0>, TR>]
: ExtractObj<S, T0> extends never
? readonly []
: readonly [T0]
: readonly []
class Store<S extends object> {
subscribe<T extends readonly unknown[]>(path: T extends Path<S, T> ? T : never) {}
}
type StoreExample = {
prop1: {
nested1: {
nested1a: string
nested1b: number
}
}
prop2: {
nested20: boolean
nested21: {
nested21a: number
}
}
}
const store = new Store<StoreExample>()
// Valid
store.subscribe(['prop1'] as const)
store.subscribe(['prop1', 'nested1', 'nested1a'] as const)
store.subscribe(['prop2', 'nested20'] as const)
store.subscribe(['prop2', 'nested21', 'nested21a'] as const)
// Invalid
store.subscribe(['prop3'] as const)
store.subscribe(['prop1', 'nested20'] as const)
store.subscribe(['prop1', 'nested1', 'nested1a', 'something'] as const) |
@aigoncharov thanks for this! It appears you can also remove the need for - subscribe<T extends readonly unknown[]>(path: T extends Path<S, T> ? T : never) {}
+ subscribe<T extends readonly [keyof S, ...unknown[]]>(path: T extends Path<S, T> ? T : never) {} |
similiar here: |
niubility |
Recently introduces
keyof
operator will work well for typing functions that accept properties directly on target type.What do you think, is it possible in theory to type deeply nested paths, so that for type:
so that it would be possible to restrict possible path values to:
This actual for such methods like ramda's
path
:?
The text was updated successfully, but these errors were encountered: