Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-keys-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Fix `DeepKeys` and `DeepValue` failing with "Type instantiation is excessively deep and possibly infinite" (TS2589) for self-referencing types such as recursive JSON types. Expansion of a path now stops when a container type repeats on the path, and the remaining path is typed as an unknown accessor that also accepts consecutive array indexes such as `data[0][0]`. Types without self-references are unchanged.
75 changes: 68 additions & 7 deletions packages/form-core/src/util-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ export type DeepKeyAndValueArray<
TParent extends AnyDeepKeyAndValue,
T extends ReadonlyArray<any>,
TAcc,
TVisited = never,
> = DeepKeysAndValuesImpl<
NonNullable<T[number]>,
ArrayDeepKeyAndValue<TParent, T>,
TAcc | ArrayDeepKeyAndValue<TParent, T>
TAcc | ArrayDeepKeyAndValue<TParent, T>,
TVisited | T
>

export type TupleAccessor<
Expand All @@ -86,11 +88,13 @@ export type DeepKeyAndValueTuple<
T extends ReadonlyArray<any>,
TAcc,
TAllKeys extends AllTupleKeys<T> = AllTupleKeys<T>,
TVisited = never,
> = TAllKeys extends any
? DeepKeysAndValuesImpl<
NonNullable<T[TAllKeys]>,
TupleDeepKeyAndValue<TParent, T, TAllKeys>,
TAcc | TupleDeepKeyAndValue<TParent, T, TAllKeys>
TAcc | TupleDeepKeyAndValue<TParent, T, TAllKeys>,
TVisited | T
>
: never

Expand Down Expand Up @@ -125,11 +129,13 @@ export type DeepKeyAndValueObject<
T,
TAcc,
TAllKeys extends AllObjectKeys<T> = AllObjectKeys<T>,
TVisited = never,
> = TAllKeys extends any
? DeepKeysAndValuesImpl<
NonNullable<T[TAllKeys]>,
ObjectDeepKeyAndValue<TParent, T, TAllKeys>,
TAcc | ObjectDeepKeyAndValue<TParent, T, TAllKeys>
TAcc | ObjectDeepKeyAndValue<TParent, T, TAllKeys>,
TVisited | T
>
: never

Expand All @@ -143,29 +149,84 @@ export interface UnknownDeepKeyAndValue<
value: unknown
}

/**
* The accessor of a path below a point where a self-referencing type was cut
* off. Unlike `UnknownAccessor` it also accepts a bracket suffix, because the
* cut type can be an array that is directly nested in another array
* (e.g. `data[0][0]` for `type Json = Json[] | { [key: string]: Json }`).
*/
export type RecursiveAccessor<TParent extends AnyDeepKeyAndValue> =
TParent['key'] extends never
? string
:
| `${TParent['key']}.${string}`
| `${TParent['key']}[${number}]`
| `${TParent['key']}[${number}].${string}`
| `${TParent['key']}[${number}][${string}`

export interface RecursiveDeepKeyAndValue<
TParent extends AnyDeepKeyAndValue,
> extends AnyDeepKeyAndValue {
key: RecursiveAccessor<TParent>
value: unknown
}

export type DeepKeysAndValues<T> =
DeepKeysAndValuesImpl<T> extends AnyDeepKeyAndValue
? DeepKeysAndValuesImpl<T>
: never

type IsIdentical<A, B> =
(<G>() => G extends A ? 1 : 2) extends <G>() => G extends B ? 1 : 2
? true
: false

/**
* @private
* Whether `T` is identical to one of the container types (arrays, tuples and
* objects) that are already being expanded further up the current path.
*/
type IsVisited<T, TVisited> = [TVisited] extends [never]
? false
: true extends (TVisited extends any ? IsIdentical<T, TVisited> : never)
? true
: false

/**
* `TVisited` holds the container types on the path from the root to `T`. A
* type that contains itself (e.g. `type Json = Json[] | { [k: string]: Json }`)
* would otherwise be expanded forever, so when a container is reached again the
* recursion stops and the path continues as an unknown accessor.
*/
export type DeepKeysAndValuesImpl<
T,
TParent extends AnyDeepKeyAndValue = never,
TAcc = never,
TVisited = never,
> = unknown extends T
? TAcc | UnknownDeepKeyAndValue<TParent>
: unknown extends T // this stops runaway recursion when T is any
? T
: T extends string | number | boolean | bigint | Date
? TAcc
: T extends ReadonlyArray<any>
? number extends T['length']
? DeepKeyAndValueArray<TParent, T, TAcc>
: DeepKeyAndValueTuple<TParent, T, TAcc>
? true extends IsVisited<T, TVisited>
? TAcc | RecursiveDeepKeyAndValue<TParent>
: number extends T['length']
? DeepKeyAndValueArray<TParent, T, TAcc, TVisited>
: DeepKeyAndValueTuple<TParent, T, TAcc, AllTupleKeys<T>, TVisited>
: keyof T extends never
? TAcc | UnknownDeepKeyAndValue<TParent>
: T extends object
? DeepKeyAndValueObject<TParent, T, TAcc>
? true extends IsVisited<T, TVisited>
? TAcc | RecursiveDeepKeyAndValue<TParent>
: DeepKeyAndValueObject<
TParent,
T,
TAcc,
AllObjectKeys<T>,
TVisited
>
: TAcc

export type DeepRecord<T> = {
Expand Down
117 changes: 117 additions & 0 deletions packages/form-core/tests/util-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,120 @@ describe('FieldsMap', () => {
expectTypeOf<TopLevelObject>().toBeNever()
})
})

describe('self-referencing types', () => {
// https://github.com/TanStack/form/issues/1474
type JsonData =
| string
| number
| boolean
| null
| JsonData[]
| { [key: string]: JsonData }

type JsonForm = { name: string; data: JsonData }

// https://github.com/TanStack/form/issues/1484
type FieldValues = {
[key: string]:
| string
| string[]
| number
| boolean
| null
| FieldValues
| FieldValues[]
}

it('should stop expanding a recursive union at the point where it repeats', () => {
type Keys = DeepKeys<JsonForm>

expectTypeOf<'name'>().toExtend<Keys>()
expectTypeOf<'data'>().toExtend<Keys>()
expectTypeOf<'data[0]'>().toExtend<Keys>()
expectTypeOf<'data.foo'>().toExtend<Keys>()
expectTypeOf<'data.foo[0].bar'>().toExtend<Keys>()
expectTypeOf<'data[0].foo.bar[1]'>().toExtend<Keys>()
expectTypeOf<'unknown'>().not.toExtend<Keys>()
expectTypeOf<'name.foo'>().not.toExtend<Keys>()
})

it('should accept consecutive array indexes below the point where a recursive array repeats', () => {
type Keys = DeepKeys<JsonForm>

expectTypeOf<'data[0][0]'>().toExtend<Keys>()
expectTypeOf<'data[0][0][1]'>().toExtend<Keys>()
expectTypeOf<'data[0][0].foo'>().toExtend<Keys>()
expectTypeOf<'data[0][0].foo[1][2].bar'>().toExtend<Keys>()
expectTypeOf<'data.foo[0][0]'>().toExtend<Keys>()
expectTypeOf<'data[0].foo[0][1]'>().toExtend<Keys>()
expectTypeOf<'data[0]foo'>().not.toExtend<Keys>()
expectTypeOf<'data[foo]'>().not.toExtend<Keys>()
expectTypeOf<'name[0]'>().not.toExtend<Keys>()
})

it('should resolve the value of a path with consecutive array indexes', () => {
expectTypeOf<DeepValue<JsonForm, 'data[0][0]'>>().not.toBeNever()
expectTypeOf<DeepValue<JsonForm, 'data[0][0].foo'>>().not.toBeNever()
})

it('should stop expanding a recursive index signature at the point where it repeats', () => {
expectTypeOf<DeepKeys<FieldValues>>().toEqualTypeOf<string>()
})

it('should resolve the value of a path through a recursive type', () => {
expectTypeOf<DeepValue<JsonForm, 'name'>>().toEqualTypeOf<string>()
expectTypeOf<DeepValue<JsonForm, 'data'>>().toEqualTypeOf<JsonData>()
expectTypeOf<DeepValue<JsonForm, 'data.foo'>>().toEqualTypeOf<JsonData>()
expectTypeOf<
DeepValue<JsonForm, 'data.foo[0].bar'>
>().toEqualTypeOf<JsonData>()
expectTypeOf<DeepValue<FieldValues, 'foo'>>().not.toBeNever()
})

it('should not treat a repeated non-recursive type as recursive', () => {
type Address = { street: string; geo: { lat: number; lng: number } }
type Shared = {
home: Address
work: Address
history: Address[]
pair: [Address, Address]
}

expectTypeOf<DeepKeys<Shared>>().toEqualTypeOf<
| 'home'
| 'home.street'
| 'home.geo'
| 'home.geo.lat'
| 'home.geo.lng'
| 'work'
| 'work.street'
| 'work.geo'
| 'work.geo.lat'
| 'work.geo.lng'
| 'history'
| `history[${number}]`
| `history[${number}].street`
| `history[${number}].geo`
| `history[${number}].geo.lat`
| `history[${number}].geo.lng`
| 'pair'
| 'pair[0]'
| 'pair[0].street'
| 'pair[0].geo'
| 'pair[0].geo.lat'
| 'pair[0].geo.lng'
| 'pair[1]'
| 'pair[1].street'
| 'pair[1].geo'
| 'pair[1].geo.lat'
| 'pair[1].geo.lng'
>()
})

it('should keep optional-only objects nested in a similar object', () => {
type Weak = { a?: string; b?: { a?: string } }

expectTypeOf<DeepKeys<Weak>>().toEqualTypeOf<'a' | 'b' | 'b.a'>()
})
})