diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts
index ad675c1f6..f8833f6a7 100644
--- a/packages/form-core/src/util-types.ts
+++ b/packages/form-core/src/util-types.ts
@@ -21,35 +21,58 @@ export type Narrow = Try>
type IsAny = 0 extends 1 & T ? true : false
-export type ArrayAccessor = `${TPrefix}[${number}]`
+export interface AnyDeepKeyAndValue {
+ key: string
+ value: any
+}
+
+export type ArrayAccessor =
+ `${TParent['key'] extends never ? '' : TParent['key']}[${number}]`
+
+export interface ArrayDeepKeyAndValue<
+ in out TParent extends AnyDeepKeyAndValue,
+ in out T extends ReadonlyArray,
+> {
+ key: ArrayAccessor
+ value: T[number] | Nullable
+}
-export type DeepRecordArrayUnion<
+export type DeepKeyAndValueArray<
+ TParent extends AnyDeepKeyAndValue,
T extends ReadonlyArray,
- TPrefix extends string,
TAcc,
-> = DeepRecordUnion<
- T[number],
- ArrayAccessor,
- TAcc | Record, T[number]>
+> = DeepKeysAndValues<
+ NonNullable,
+ ArrayDeepKeyAndValue,
+ TAcc | ArrayDeepKeyAndValue
>
export type TupleAccessor<
- TPrefix extends string,
- TKey,
-> = `${TPrefix}[${TKey & string}]`
+ TParent extends AnyDeepKeyAndValue,
+ TKey extends string,
+> = `${TParent['key'] extends never ? '' : TParent['key']}[${TKey}]`
+
+export interface TupleDeepKeyAndValue<
+ in out TParent extends AnyDeepKeyAndValue,
+ in out T,
+ in out TKey extends AllTupleKeys,
+> {
+ key: TupleAccessor
+ value: T[TKey] | Nullable
+}
export type AllTupleKeys = T extends any ? keyof T & `${number}` : never
-export type DeepRecordTupleUnion<
+export type DeepKeyAndValueTuple<
+ TParent extends AnyDeepKeyAndValue,
T extends ReadonlyArray,
- TPrefix extends string,
TAcc,
TAllKeys extends AllTupleKeys = AllTupleKeys,
> = TAllKeys extends any
- ? DeepRecordUnion<
- T[TAllKeys],
- TupleAccessor,
- TAcc | Record, T[TAllKeys]>
+ ? DeepKeysAndValues<
+ NonNullable,
+ TupleDeepKeyAndValue,
+ TAcc | TupleDeepKeyAndValue
>
: never
@@ -58,53 +81,85 @@ export type AllObjectKeys = T extends any
: never
export type ObjectAccessor<
- TPrefix extends string,
+ TParent extends AnyDeepKeyAndValue,
TKey extends string | number,
-> = TPrefix extends '' ? `${TKey}` : `${TPrefix}.${TKey}`
+> = TParent['key'] extends never ? `${TKey}` : `${TParent['key']}.${TKey}`
+
+export type Nullable = T & (undefined | null)
+
+export interface ObjectDeepKeyAndValue<
+ in out TParent extends AnyDeepKeyAndValue,
+ in out T,
+ in out TKey extends AllObjectKeys,
+> {
+ key: ObjectAccessor
+ value: T[TKey] | Nullable
+}
-export type DeepRecordObjectUnion<
+export type DeepKeyAndValueObject<
+ TParent extends AnyDeepKeyAndValue,
T,
- TPrefix extends string,
TAcc,
TAllKeys extends AllObjectKeys = AllObjectKeys,
> = TAllKeys extends any
- ? DeepRecordUnion<
- T[TAllKeys],
- ObjectAccessor,
- TAcc | Record, T[TAllKeys]>
+ ? DeepKeysAndValues<
+ NonNullable,
+ ObjectDeepKeyAndValue,
+ TAcc | ObjectDeepKeyAndValue
>
: never
-export type DeepRecordUnion =
+export type UnknownAccessor =
+ TParent['key'] extends never ? string : `${TParent['key']}.${string}`
+
+export interface UnknownDeepKeyAndValue {
+ key: UnknownAccessor
+ value: unknown
+}
+
+export type DeepKeyAndValueUnknown =
+ UnknownDeepKeyAndValue
+
+export type DeepKeysAndValues<
+ T,
+ TParent extends AnyDeepKeyAndValue = never,
+ TAcc = never,
+> =
IsAny extends true
? T
: T extends string | number | boolean | bigint | Date
? TAcc
: T extends ReadonlyArray
? number extends T['length']
- ? DeepRecordArrayUnion
- : DeepRecordTupleUnion
- : T extends object
- ? DeepRecordObjectUnion
- : TAcc
+ ? DeepKeyAndValueArray
+ : DeepKeyAndValueTuple
+ : keyof T extends never
+ ? TAcc | DeepKeyAndValueUnknown
+ : T extends object
+ ? DeepKeyAndValueObject
+ : TAcc
export type DeepRecord = {
- [TRecord in DeepRecordUnion as keyof TRecord]: TRecord[keyof TRecord]
+ [TRecord in DeepKeysAndValues extends AnyDeepKeyAndValue
+ ? DeepKeysAndValues
+ : never as TRecord['key']]: TRecord['value']
}
-type UnionKeys = T extends any ? keyof T : never
-
/**
* The keys of an object or array, deeply nested.
*/
export type DeepKeys = unknown extends T
? string
- : UnionKeys> & string
+ : DeepKeysAndValues extends AnyDeepKeyAndValue
+ ? DeepKeysAndValues['key']
+ : never
/**
* Infer the type of a deeply nested property within an object or an array.
*/
export type DeepValue =
DeepRecord extends infer TDeepRecord
- ? TDeepRecord[TAccessor & keyof TDeepRecord]
+ ? TAccessor extends keyof TDeepRecord
+ ? TDeepRecord[TAccessor]
+ : never
: never
diff --git a/packages/form-core/tests/FieldApi.test-d.ts b/packages/form-core/tests/FieldApi.test-d.ts
index fcbbc1f60..92ef5c99b 100644
--- a/packages/form-core/tests/FieldApi.test-d.ts
+++ b/packages/form-core/tests/FieldApi.test-d.ts
@@ -134,7 +134,7 @@ it('should type an array sub-field properly', () => {
const field = new FieldApi({
form,
- name: `nested.people[${1}].name`,
+ name: `nested.people[1].name`,
validators: {
onChangeAsync: async ({ value }) => {
assertType(value)
diff --git a/packages/form-core/tests/util-types.test-d.ts b/packages/form-core/tests/util-types.test-d.ts
index b4a55ef45..db835ee54 100644
--- a/packages/form-core/tests/util-types.test-d.ts
+++ b/packages/form-core/tests/util-types.test-d.ts
@@ -1,11 +1,11 @@
-import { assertType } from 'vitest'
+import { expectTypeOf } from 'vitest'
import type { DeepKeys, DeepValue } from '../src/index'
/**
* Properly recognizes that `0` is not an object and should not have subkeys
*/
type TupleSupport = DeepKeys<{ topUsers: [User, 0, User] }>
-assertType<
+expectTypeOf(0 as never as TupleSupport).toEqualTypeOf<
| 'topUsers'
| 'topUsers[0]'
| 'topUsers[0].name'
@@ -16,65 +16,65 @@ assertType<
| 'topUsers[2].name'
| 'topUsers[2].id'
| 'topUsers[2].age'
->(0 as never as TupleSupport)
+>()
/**
* Properly recognizes that a normal number index won't cut it and should be `[number]` prefixed instead
*/
type ArraySupport = DeepKeys<{ users: User[] }>
-assertType<
+expectTypeOf(0 as never as ArraySupport).toEqualTypeOf<
| 'users'
| `users[${number}]`
| `users[${number}].name`
| `users[${number}].id`
| `users[${number}].age`
->(0 as never as ArraySupport)
+>()
/**
* Properly handles deep object nesting like so:
*/
type NestedSupport = DeepKeys<{ meta: { mainUser: User } }>
-assertType<
+expectTypeOf(0 as never as NestedSupport).toEqualTypeOf<
| 'meta'
| 'meta.mainUser'
| 'meta.mainUser.name'
| 'meta.mainUser.id'
| 'meta.mainUser.age'
->(0 as never as NestedSupport)
+>()
/**
* Properly handles deep partial object nesting like so:
*/
type NestedPartialSupport = DeepKeys<{ meta?: { mainUser?: User } }>
-assertType<
+expectTypeOf(0 as never as NestedPartialSupport).toEqualTypeOf<
| 'meta'
| 'meta.mainUser'
| 'meta.mainUser.name'
| 'meta.mainUser.id'
| 'meta.mainUser.age'
->(0 as never as NestedPartialSupport)
+>()
/**
* Properly handles `object` edgecase nesting like so:
*/
type ObjectNestedEdgecase = DeepKeys<{ meta: { mainUser: object } }>
-assertType<'meta' | 'meta.mainUser' | `meta.mainUser.${string}`>(
- 0 as never as ObjectNestedEdgecase,
+expectTypeOf(0 as never as ObjectNestedEdgecase).toEqualTypeOf(
+ 0 as never as 'meta' | 'meta.mainUser' | `meta.mainUser.${string}`,
)
/**
* Properly handles `object` edgecase like so:
*/
type ObjectEdgecase = DeepKeys