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 -assertType(0 as never as ObjectEdgecase) +expectTypeOf(0 as never as ObjectEdgecase).toEqualTypeOf() /** * Properly handles `object` edgecase nesting like so: */ type UnknownNestedEdgecase = DeepKeys<{ meta: { mainUser: unknown } }> -assertType<'meta' | 'meta.mainUser' | `meta.mainUser.${string}`>( - 0 as never as UnknownNestedEdgecase, -) +expectTypeOf( + 0 as never as 'meta' | 'meta.mainUser' | `meta.mainUser.${string}`, +).toEqualTypeOf(0 as never as UnknownNestedEdgecase) /** * Properly handles discriminated unions like so: @@ -84,24 +84,30 @@ type DiscriminatedUnion = { name: string } & ( | { variant: 'bar'; baz: boolean } ) type DiscriminatedUnionKeys = DeepKeys -assertType<'name' | 'variant' | 'baz'>(0 as never as DiscriminatedUnionKeys) +expectTypeOf(0 as never as DiscriminatedUnionKeys).toEqualTypeOf< + 'name' | 'variant' | 'baz' +>() type DiscriminatedUnionValueShared = DeepValue -assertType<'foo' | 'bar'>(0 as never as DiscriminatedUnionValueShared) +expectTypeOf(0 as never as DiscriminatedUnionValueShared).toEqualTypeOf< + 'foo' | 'bar' +>() type DiscriminatedUnionValueFixed = DeepValue -assertType(0 as never as DiscriminatedUnionValueFixed) +expectTypeOf( + 0 as never as DiscriminatedUnionValueFixed, +).toEqualTypeOf() /** * Properly handles `object` edgecase like so: */ type UnknownEdgecase = DeepKeys -assertType(0 as never as UnknownEdgecase) +expectTypeOf(0 as never as UnknownEdgecase).toEqualTypeOf() type NestedKeysExample = DeepValue< { meta: { mainUser: User } }, 'meta.mainUser.age' > -assertType(0 as never as NestedKeysExample) +expectTypeOf(0 as never as NestedKeysExample).toEqualTypeOf() type NestedNullableObjectCase = { null: { mainUser: 'name' } | null @@ -114,22 +120,28 @@ type NestedNullableObjectCaseNull = DeepValue< NestedNullableObjectCase, 'null.mainUser' > -assertType<'name' | null>(0 as never as NestedNullableObjectCaseNull) +expectTypeOf(0 as never as NestedNullableObjectCaseNull).toEqualTypeOf< + 'name' | null +>() type NestedNullableObjectCaseUndefined = DeepValue< NestedNullableObjectCase, 'undefined.mainUser' > -assertType<'name' | undefined>(0 as never as NestedNullableObjectCaseUndefined) +expectTypeOf(0 as never as NestedNullableObjectCaseUndefined).toEqualTypeOf< + 'name' | undefined +>() type NestedNullableObjectCaseOptional = DeepValue< NestedNullableObjectCase, 'undefined.mainUser' > -assertType<'name' | undefined>(0 as never as NestedNullableObjectCaseOptional) +expectTypeOf(0 as never as NestedNullableObjectCaseOptional).toEqualTypeOf< + 'name' | undefined +>() type NestedNullableObjectCaseMixed = DeepValue< NestedNullableObjectCase, 'mixed.mainUser' > -assertType<'name' | null | undefined>( +expectTypeOf(0 as never as 'name' | null | undefined).toEqualTypeOf( 0 as never as NestedNullableObjectCaseMixed, ) @@ -140,14 +152,16 @@ type DoubleNestedNullableObjectA = DeepValue< DoubleNestedNullableObjectCase, 'mixed.mainUser' > -assertType<{ name: 'name' } | null | undefined>( +expectTypeOf(0 as never as { name: 'name' } | null | undefined).toEqualTypeOf( 0 as never as DoubleNestedNullableObjectA, ) type DoubleNestedNullableObjectB = DeepValue< DoubleNestedNullableObjectCase, 'mixed.mainUser.name' > -assertType<'name' | null | undefined>(0 as never as DoubleNestedNullableObjectB) +expectTypeOf(0 as never as DoubleNestedNullableObjectB).toEqualTypeOf< + 'name' | null | undefined +>() type NestedObjectUnionCase = { normal: @@ -157,11 +171,11 @@ type NestedObjectUnionCase = { | { c: { user: User } | { user: number } } } type NestedObjectUnionA = DeepValue -assertType(0 as never as NestedObjectUnionA) +expectTypeOf(0 as never as NestedObjectUnionA).toEqualTypeOf() type NestedObjectUnionB = DeepValue -assertType(0 as never as NestedObjectUnionB) +expectTypeOf(0 as never as NestedObjectUnionB).toEqualTypeOf() type NestedObjectUnionC = DeepValue -assertType(0 as never as NestedObjectUnionC) +expectTypeOf(0 as never as NestedObjectUnionC).toEqualTypeOf() type NestedNullableObjectUnionCase = { nullable: @@ -172,67 +186,76 @@ type NestedNullableObjectUnionA = DeepValue< NestedNullableObjectUnionCase, 'nullable.a' > -assertType(0 as never as NestedNullableObjectUnionA) +expectTypeOf(0 as never as NestedNullableObjectUnionA).toEqualTypeOf< + number | undefined +>() type NestedNullableObjectUnionB = DeepValue< NestedNullableObjectUnionCase, 'nullable.b.c' > -assertType( +expectTypeOf(0 as never as string | boolean | null | undefined).toEqualTypeOf( 0 as never as NestedNullableObjectUnionB, ) type NestedNullableObjectUnionC = DeepValue< NestedNullableObjectUnionCase, 'nullable.b.e' > -assertType(0 as never as NestedNullableObjectUnionC) +expectTypeOf(0 as never as NestedNullableObjectUnionC).toEqualTypeOf< + number | undefined +>() type NestedArrayExample = DeepValue<{ users: User[] }, 'users[0].age'> -assertType(0 as never as NestedArrayExample) +expectTypeOf(0 as never as NestedArrayExample).toEqualTypeOf() -type NestedLooseArrayExample = DeepValue<{ users: User[] }, 'users[number].age'> -assertType(0 as never as NestedLooseArrayExample) +type NestedLooseArrayExample = DeepValue< + { users: User[] }, + `users[${number}].age` +> +expectTypeOf(0 as never as NestedLooseArrayExample).toEqualTypeOf() type NestedArrayUnionExample = DeepValue< { users: string | User[] }, 'users[0].age' > -assertType(0 as never as NestedArrayUnionExample) +expectTypeOf(0 as never as NestedArrayUnionExample).toEqualTypeOf() type NestedTupleExample = DeepValue< { topUsers: [User, 0, User] }, 'topUsers[0].age' > -assertType(0 as never as NestedTupleExample) +expectTypeOf(0 as never as NestedTupleExample).toEqualTypeOf() type NestedTupleBroadExample = DeepValue< { topUsers: User[] }, `topUsers[${number}].age` > -assertType(0 as never as NestedTupleBroadExample) +expectTypeOf(0 as never as NestedTupleBroadExample).toEqualTypeOf() type DeeplyNestedTupleBroadExample = DeepValue< { nested: { topUsers: User[] } }, `nested.topUsers[${number}].age` > -assertType(0 as never as DeeplyNestedTupleBroadExample) +expectTypeOf( + 0 as never as DeeplyNestedTupleBroadExample, +).toEqualTypeOf() type SimpleArrayExample = DeepValue -assertType(0 as never as SimpleArrayExample) +expectTypeOf(0 as never as SimpleArrayExample).toEqualTypeOf() type SimpleNestedArrayExample = DeepValue -assertType(0 as never as SimpleNestedArrayExample) +expectTypeOf(0 as never as SimpleNestedArrayExample).toEqualTypeOf() type NestedTupleItemExample = DeepValue< { topUsers: [User, 0, User] }, 'topUsers[1]' > -assertType<0>(0 as never as NestedTupleItemExample) +expectTypeOf(0 as never as NestedTupleItemExample).toEqualTypeOf<0>() type ArrayExample = DeepValue<[1, 2, 3], '[1]'> -assertType<2>(0 as never as ArrayExample) +expectTypeOf(0 as never as ArrayExample).toEqualTypeOf<2>() type NonNestedObjExample = DeepValue<{ a: 1 }, 'a'> -assertType<1>(0 as never as NonNestedObjExample) +expectTypeOf(0 as never as NonNestedObjExample).toEqualTypeOf<1>() interface User { name: string @@ -251,7 +274,7 @@ type FormDefinitionValue = DeepValue< `nested.people[${number}].name` > -assertType(0 as never as FormDefinitionValue) +expectTypeOf(0 as never as FormDefinitionValue).toEqualTypeOf() type DoubleDeepArray = DeepValue< { @@ -265,7 +288,7 @@ type DoubleDeepArray = DeepValue< `people[${0}].parents[${0}].name` > -assertType(0 as never as DoubleDeepArray) +expectTypeOf(0 as never as DoubleDeepArray).toEqualTypeOf() // Deepness is infinite error check type Cart = {