Skip to content

fix(form-core): types to pass most tests #1411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 15, 2025
Merged
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
125 changes: 90 additions & 35 deletions packages/form-core/src/util-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,58 @@ export type Narrow<A> = Try<A, [], NarrowRaw<A>>

type IsAny<T> = 0 extends 1 & T ? true : false

export type ArrayAccessor<TPrefix extends string> = `${TPrefix}[${number}]`
export interface AnyDeepKeyAndValue {
key: string
value: any
}

export type ArrayAccessor<TParent extends AnyDeepKeyAndValue> =
`${TParent['key'] extends never ? '' : TParent['key']}[${number}]`

export interface ArrayDeepKeyAndValue<
in out TParent extends AnyDeepKeyAndValue,
in out T extends ReadonlyArray<any>,
> {
key: ArrayAccessor<TParent>
value: T[number] | Nullable<TParent['value']>
}

export type DeepRecordArrayUnion<
export type DeepKeyAndValueArray<
TParent extends AnyDeepKeyAndValue,
T extends ReadonlyArray<any>,
TPrefix extends string,
TAcc,
> = DeepRecordUnion<
T[number],
ArrayAccessor<TPrefix>,
TAcc | Record<ArrayAccessor<TPrefix>, T[number]>
> = DeepKeysAndValues<
NonNullable<T[number]>,
ArrayDeepKeyAndValue<TParent, T>,
TAcc | ArrayDeepKeyAndValue<TParent, T>
>

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<T>,
> {
key: TupleAccessor<TParent, TKey>
value: T[TKey] | Nullable<TParent['value']>
}

export type AllTupleKeys<T> = T extends any ? keyof T & `${number}` : never

export type DeepRecordTupleUnion<
export type DeepKeyAndValueTuple<
TParent extends AnyDeepKeyAndValue,
T extends ReadonlyArray<any>,
TPrefix extends string,
TAcc,
TAllKeys extends AllTupleKeys<T> = AllTupleKeys<T>,
> = TAllKeys extends any
? DeepRecordUnion<
T[TAllKeys],
TupleAccessor<TPrefix, TAllKeys>,
TAcc | Record<TupleAccessor<TPrefix, TAllKeys>, T[TAllKeys]>
? DeepKeysAndValues<
NonNullable<T[TAllKeys]>,
TupleDeepKeyAndValue<TParent, T, TAllKeys>,
TAcc | TupleDeepKeyAndValue<TParent, T, TAllKeys>
>
: never

Expand All @@ -58,53 +81,85 @@ export type AllObjectKeys<T> = 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> = T & (undefined | null)

export interface ObjectDeepKeyAndValue<
in out TParent extends AnyDeepKeyAndValue,
in out T,
in out TKey extends AllObjectKeys<T>,
> {
key: ObjectAccessor<TParent, TKey>
value: T[TKey] | Nullable<TParent['value']>
}

export type DeepRecordObjectUnion<
export type DeepKeyAndValueObject<
TParent extends AnyDeepKeyAndValue,
T,
TPrefix extends string,
TAcc,
TAllKeys extends AllObjectKeys<T> = AllObjectKeys<T>,
> = TAllKeys extends any
? DeepRecordUnion<
T[TAllKeys],
ObjectAccessor<TPrefix, TAllKeys>,
TAcc | Record<ObjectAccessor<TPrefix, TAllKeys>, T[TAllKeys]>
? DeepKeysAndValues<
NonNullable<T[TAllKeys]>,
ObjectDeepKeyAndValue<TParent, T, TAllKeys>,
TAcc | ObjectDeepKeyAndValue<TParent, T, TAllKeys>
>
: never

export type DeepRecordUnion<T, TPrefix extends string = '', TAcc = never> =
export type UnknownAccessor<TParent extends AnyDeepKeyAndValue> =
TParent['key'] extends never ? string : `${TParent['key']}.${string}`

export interface UnknownDeepKeyAndValue<TParent extends AnyDeepKeyAndValue> {
key: UnknownAccessor<TParent>
value: unknown
}

export type DeepKeyAndValueUnknown<TParent extends AnyDeepKeyAndValue> =
UnknownDeepKeyAndValue<TParent>

export type DeepKeysAndValues<
T,
TParent extends AnyDeepKeyAndValue = never,
TAcc = never,
> =
IsAny<T> extends true
? T
: T extends string | number | boolean | bigint | Date
? TAcc
: T extends ReadonlyArray<any>
? number extends T['length']
? DeepRecordArrayUnion<T, TPrefix, TAcc>
: DeepRecordTupleUnion<T, TPrefix, TAcc>
: T extends object
? DeepRecordObjectUnion<T, TPrefix, TAcc>
: TAcc
? DeepKeyAndValueArray<TParent, T, TAcc>
: DeepKeyAndValueTuple<TParent, T, TAcc>
: keyof T extends never
? TAcc | DeepKeyAndValueUnknown<TParent>
: T extends object
? DeepKeyAndValueObject<TParent, T, TAcc>
: TAcc

export type DeepRecord<T> = {
[TRecord in DeepRecordUnion<T> as keyof TRecord]: TRecord[keyof TRecord]
[TRecord in DeepKeysAndValues<T> extends AnyDeepKeyAndValue
? DeepKeysAndValues<T>
: never as TRecord['key']]: TRecord['value']
}

type UnionKeys<T> = T extends any ? keyof T : never

/**
* The keys of an object or array, deeply nested.
*/
export type DeepKeys<T> = unknown extends T
? string
: UnionKeys<DeepRecordUnion<T>> & string
: DeepKeysAndValues<T> extends AnyDeepKeyAndValue
? DeepKeysAndValues<T>['key']
: never

/**
* Infer the type of a deeply nested property within an object or an array.
*/
export type DeepValue<TValue, TAccessor> =
DeepRecord<TValue> extends infer TDeepRecord
? TDeepRecord[TAccessor & keyof TDeepRecord]
? TAccessor extends keyof TDeepRecord
? TDeepRecord[TAccessor]
: never
: never
2 changes: 1 addition & 1 deletion packages/form-core/tests/FieldApi.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(value)
Expand Down
Loading