Skip to content

Commit a629bc3

Browse files
fix(deep-keys): fix recursive types (#327)
1 parent b3e375f commit a629bc3

File tree

4 files changed

+155
-13
lines changed

4 files changed

+155
-13
lines changed

.changeset/slimy-ways-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/devtools-ui': patch
3+
---
4+
5+
Fixes the deep-keys utils for the collapsePath prop, now handles any and unknown types.

knip.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"entry": ["**/vite.config.solid.ts", "**/src/solid/**"],
99
"project": ["**/vite.config.solid.ts", "**/src/solid/**"]
1010
},
11+
"packages/devtools-ui": {
12+
"entry": ["**/tests/**/*.ts"]
13+
},
1114
"packages/solid-devtools": {
1215
"ignore": ["**/core.tsx"]
1316
}
Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1+
type IsAny<T> = 0 extends 1 & T ? true : false
2+
3+
type Clean<T> = Exclude<T, undefined | null>
4+
15
type CollapsibleKeys<T, TPrefix extends string = ''> =
2-
T extends ReadonlyArray<infer U>
3-
?
4-
| (TPrefix extends '' ? '' : TPrefix)
5-
| CollapsibleKeys<U, `${TPrefix}[${number}]`>
6-
: T extends object
6+
IsAny<T> extends true
7+
? TPrefix extends ''
8+
? never
9+
: TPrefix
10+
: T extends ReadonlyArray<infer U>
711
?
812
| (TPrefix extends '' ? '' : TPrefix)
9-
| {
10-
[K in Extract<keyof T, string>]: CollapsibleKeys<
11-
T[K],
12-
TPrefix extends '' ? `${K}` : `${TPrefix}.${K}`
13-
>
14-
}[Extract<keyof T, string>]
15-
: never
13+
| CollapsibleKeys<U, `${TPrefix}[${number}]`>
14+
: T extends object
15+
?
16+
| (TPrefix extends '' ? '' : TPrefix)
17+
| {
18+
[K in Extract<keyof T, string>]: CollapsibleKeys<
19+
T[K],
20+
TPrefix extends '' ? `${K}` : `${TPrefix}.${K}`
21+
>
22+
}[Extract<keyof T, string>]
23+
: never
1624

1725
export type CollapsiblePaths<T> =
18-
CollapsibleKeys<T> extends string ? CollapsibleKeys<T> : never
26+
CollapsibleKeys<Clean<T>> extends infer P
27+
? P extends string
28+
? P
29+
: never
30+
: never
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
3+
// types
4+
import type { CollapsiblePaths } from '../src/utils/deep-keys'
5+
6+
type WithDeeplyNestedObject = {
7+
a: {
8+
b: {
9+
c: {
10+
d: {
11+
e: {
12+
f: {
13+
g: {
14+
h: {
15+
i: {
16+
j: number
17+
}
18+
}
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
28+
type _DeeplyNestedObject = CollapsiblePaths<WithDeeplyNestedObject>
29+
30+
type WithAny = {
31+
errors?: any
32+
}
33+
34+
type _Any = CollapsiblePaths<WithAny>
35+
36+
type ArrayRecursion = { arr: Array<Array<Array<Array<[]>>>> }
37+
38+
type _ArrayRecursion = CollapsiblePaths<ArrayRecursion>
39+
40+
type WithUndefined = {
41+
status?: {
42+
valid: boolean
43+
error?: {
44+
message: string
45+
}
46+
}
47+
}
48+
49+
type _WithUndefined = CollapsiblePaths<WithUndefined>
50+
51+
type WithUnknown = {
52+
payload: unknown
53+
}
54+
55+
type _WithUnknown = CollapsiblePaths<WithUnknown>
56+
57+
type WithRealisticState = {
58+
canSubmit?: boolean
59+
isSubmitting?: boolean
60+
errors?: Array<any>
61+
errorMap?: Record<string, any>
62+
}
63+
64+
type _WithRealisticState = CollapsiblePaths<WithRealisticState>
65+
66+
type WithGeneric<TData> = {
67+
generic: TData
68+
}
69+
70+
type _WithGeneric = CollapsiblePaths<WithGeneric<{ a: { b: string } }>>
71+
72+
describe('deep-keys', () => {
73+
it('should type deeply nested keys', () => {
74+
expectTypeOf<_DeeplyNestedObject>().toEqualTypeOf<
75+
| ''
76+
| 'a'
77+
| 'a.b'
78+
| 'a.b.c'
79+
| 'a.b.c.d'
80+
| 'a.b.c.d.e'
81+
| 'a.b.c.d.e.f'
82+
| 'a.b.c.d.e.f.g'
83+
| 'a.b.c.d.e.f.g.h'
84+
| 'a.b.c.d.e.f.g.h.i'
85+
>()
86+
})
87+
88+
it('should handle any', () => {
89+
expectTypeOf<_Any>().toEqualTypeOf<'' | 'errors'>()
90+
})
91+
92+
it('should handle array recursion', () => {
93+
expectTypeOf<_ArrayRecursion>().toEqualTypeOf<
94+
| ''
95+
| 'arr'
96+
| `arr[${number}]`
97+
| `arr[${number}][${number}]`
98+
| `arr[${number}][${number}][${number}]`
99+
| `arr[${number}][${number}][${number}][${number}]`
100+
>()
101+
})
102+
103+
it('should handle undefined', () => {
104+
expectTypeOf<_WithUndefined>().toEqualTypeOf<
105+
'' | 'status' | 'status.error'
106+
>()
107+
})
108+
109+
it('should handle unknown', () => {
110+
expectTypeOf<_WithUnknown>().toEqualTypeOf<''>()
111+
})
112+
113+
it('should handle realistic state', () => {
114+
expectTypeOf<_WithRealisticState>().toEqualTypeOf<
115+
'' | 'errors' | 'errorMap' | `errors[${number}]` | `errorMap.${string}`
116+
>()
117+
})
118+
119+
it('should handle generics', () => {
120+
expectTypeOf<_WithGeneric>().toEqualTypeOf<'' | 'generic' | 'generic.a'>()
121+
})
122+
})

0 commit comments

Comments
 (0)