Repository navigation
Typing for object deep paths #12290
Description
Activity
HerringtonDarkholme commented
on Nov 16, 2016 ContributorMore actionsYou 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}}})
Reacted by Aluan Haddad, Ryan Cavanaugh, Petr Kosikhin, kiara, Grant Mathews, Hugo Wood, Chet Corcos, Kirill Alexander Khalitov, Aankhen, Jihad D. Waspada and 17 moreHerrington Darkholme (@HerringtonDarkholme) oh that is really really nice, thanks for the example!
- addedQuestionAn issue which isn't directly actionable in codeAn issue which isn't directly actionable in code
on Nov 16, 2016 aluanhaddad commented
on Nov 16, 2016 ContributorMore actionsHerrington Darkholme (@HerringtonDarkholme) very slick
Reacted by Petr Kosikhin, kiara, Herrington Darkholme and Petur SubevKiaraGrouwstra commented
on Dec 11, 2016 ContributorMore actionsHerrington Darkholme (@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. 😄
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 theinnotation?Reacted by Alex@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.
KiaraGrouwstra commented
on Dec 11, 2016 ContributorMore actionsYeah, 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. :DYes it seem that is also case with tuples which makes typing issue unsolvable for general case.
KiaraGrouwstra commented
on Dec 11, 2016 ContributorMore actionsRenamed in favour of (@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!Herrington Darkholme (@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; }'.KiaraGrouwstra commented
on Mar 24, 2017 ContributorMore actionsRenamed in favour of (@whitecolor): what if you make that
KeyNintonumber?On that
pathdefinition 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 originalreducelogic isO(n), so I hope they'll consider my proposal to implement that as a solution...KeyNin 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 =)
Reacted by kiaraReacted by kiaraHerringtonDarkholme commented
on Mar 24, 2017 ContributorMore actionsRenamed in favour of (@whitecolor)
The problem here is how TypeScript infer type argument. The
KeyNandKeyMwill be inferred against{ str: string; num: number; }so that compiler will infer types compatible both withextends stringandkeyof typeof obj.In such condition,
KeyNis inferred asstr | num. So the error.One solution is using curry to help compiler infer type argument.
Reacted by AlexHerrington Darkholme (@HerringtonDarkholme) Thanks for suggestion)
Any advice is it possible to solve more complicated case, to check if
targetobject, contains props oforignalobject 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 })
36 remaining items
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 Chet Corcos (@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
pathat the bottom there doesn't throw any errors when its clear to see thatcountis 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.
Chet Corcos (@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; }
Reacted by Karol Majewski and Dmytro MaslieiThis 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())Reacted by Kurt Preston and aviral-clarifai- 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
ObjectKeyPath<T>utility function, which is an array of variable length but each item conforms to the deep key path of objectT.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...
Reacted by Lucio Giannotta, Ezra Celli and Caitlin WoltersI totally much agree. Seems like this could be implemented internally with much better success. Any thoughts? Mohamed Hegazy (@mhegazy) Ryan Cavanaugh (@RyanCavanaugh)
jameslaneconkling commented
on Jun 5, 2020 More actionsFWIW, 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.
Reacted by Karol Majewski, Asger Hallas, Chris and rebelwolfsonGuys, 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)
Reacted by Andrei Pechkurov, Paul Myburgh, chocolateboy, Noah Seger, Karol Majewski, Tylor Reynolds, David, Sean Blonien, Ryan Dsouza, esmevane and 3 moreaigoncharov thanks for this! It appears you can also remove the need for
as constwith this change to the signature:- 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) {}
Reacted by Tylor Reynolds, Igor Benić and Dany Fedorovsimiliar here:
sindresorhus/type-fest#158Guys, 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)
niubility
Reacted by Ádám Rocska- locked as resolved and limited conversation to collaborators
on Oct 21, 2025
Recently introduces
keyofoperator 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:?