Skip to content
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

fix(core): add support to union types for DeepValue #724

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
42 changes: 23 additions & 19 deletions packages/form-core/src/util-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
type Nullable<T> = T | null
type IsNullable<T> = [null] extends [T] ? true : false

/**
* @private
*/
Expand Down Expand Up @@ -103,6 +100,20 @@ type PrefixFromDepth<
TDepth extends any[],
> = TDepth['length'] extends 0 ? T : `.${T}`

// Hack changing Typescript's default get behavior in order to work with union objects
type Get<T, K extends string> = T extends { [Key in K]: infer V }
? V
: T extends { [Key in K]?: infer W }
? W | undefined
: never

type ApplyNull<C, T> = null extends T ? (null extends C ? C : C | null) : C
type ApplyUndefined<C, T> = undefined extends T
? undefined extends C
? C
: C | undefined
: C

/**
* Infer the type of a deeply nested property within an object or an array.
*/
Expand All @@ -111,7 +122,6 @@ export type DeepValue<
TValue,
// A string representing the path of the property we're trying to access
TAccessor,
TNullable extends boolean = IsNullable<TValue>,
> =
// If TValue is any it will recurse forever, this terminates the recursion
unknown extends TValue
Expand All @@ -129,18 +139,12 @@ export type DeepValue<
: TAccessor extends keyof TValue
? TValue[TAccessor]
: TValue[TAccessor & number]
: // Check if we're looking for the property in an object
TValue extends Record<string | number, any>
? TAccessor extends `${infer TBefore}[${infer TEverythingElse}`
? DeepValue<DeepValue<TValue, TBefore>, `[${TEverythingElse}`>
: TAccessor extends `[${infer TBrackets}]`
? DeepValue<TValue, TBrackets>
: TAccessor extends `${infer TBefore}.${infer TAfter}`
? DeepValue<DeepValue<TValue, TBefore>, TAfter>
: TAccessor extends string
? TNullable extends true
? Nullable<TValue[TAccessor]>
: TValue[TAccessor]
: never
: // Do not allow `TValue` to be anything else
never
: TAccessor extends `${infer TBefore}[${infer TEverythingElse}`
? DeepValue<DeepValue<TValue, TBefore>, `[${TEverythingElse}`>
: TAccessor extends `[${infer TBrackets}]`
? DeepValue<TValue, TBrackets>
: TAccessor extends `${infer TBefore}.${infer TAfter}`
? DeepValue<DeepValue<TValue, TBefore>, TAfter>
: TAccessor extends `${infer TKey}`
? ApplyUndefined<ApplyNull<Get<TValue, TKey>, TValue>, TValue>
: never
24 changes: 24 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,30 @@ describe('form api', () => {
expect(form.state.errors).toStrictEqual(['first name is required'])
})

it('should read and update union objects', async () => {
const form = new FormApi({
defaultValues: {
person: { firstName: 'firstName' },
} as { person?: { firstName: string } | { age: number } | null },
})

const field = new FieldApi({
form,
name: 'person.firstName',
})
field.mount()
expect(field.getValue()).toStrictEqual('firstName')

form.setFieldValue('person', { age: 0 })

const field2 = new FieldApi({
form,
name: 'person.age',
})
field2.mount()
expect(field2.getValue()).toStrictEqual(0)
})

it('should update a nullable object', async () => {
const form = new FormApi({
defaultValues: {
Expand Down
93 changes: 87 additions & 6 deletions packages/form-core/tests/util-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,101 @@ type NestedKeysExample = DeepValue<
>
assertType<number>(0 as never as NestedKeysExample)

type NestedNullableKeys = DeepValue<
{
meta: { mainUser: 'hello' } | null
},
'meta.mainUser'
type NestedNullableObjectCase = {
null: { mainUser: 'name' } | null
undefined: { mainUser: 'name' } | undefined
optional?: { mainUser: 'name' }
mixed: { mainUser: 'name' } | null | undefined
}

type NestedNullableObjectCaseNull = DeepValue<
NestedNullableObjectCase,
'null.mainUser'
>
assertType<'name' | null>(0 as never as NestedNullableObjectCaseNull)
type NestedNullableObjectCaseUndefined = DeepValue<
NestedNullableObjectCase,
'undefined.mainUser'
>
assertType<'name' | undefined>(0 as never as NestedNullableObjectCaseUndefined)
type NestedNullableObjectCaseOptional = DeepValue<
NestedNullableObjectCase,
'undefined.mainUser'
>
assertType<'hello' | null>(0 as never as NestedNullableKeys)
assertType<'name' | undefined>(0 as never as NestedNullableObjectCaseOptional)
type NestedNullableObjectCaseMixed = DeepValue<
NestedNullableObjectCase,
'mixed.mainUser'
>
assertType<'name' | null | undefined>(
0 as never as NestedNullableObjectCaseMixed,
)

type DoubleNestedNullableObjectCase = {
mixed?: { mainUser: { name: 'name' } } | null | undefined
}
type DoubleNestedNullableObjectA = DeepValue<
DoubleNestedNullableObjectCase,
'mixed.mainUser'
>
assertType<{ name: 'name' } | null | undefined>(
0 as never as DoubleNestedNullableObjectA,
)
type DoubleNestedNullableObjectB = DeepValue<
DoubleNestedNullableObjectCase,
'mixed.mainUser.name'
>
assertType<'name' | null | undefined>(0 as never as DoubleNestedNullableObjectB)

type NestedObjectUnionCase = {
normal:
| { a: User }
| { a: string }
| { b: string }
| { c: { user: User } | { user: number } }
}
type NestedObjectUnionA = DeepValue<NestedObjectUnionCase, 'normal.a.age'>
assertType<number>(0 as never as NestedObjectUnionA)
type NestedObjectUnionB = DeepValue<NestedObjectUnionCase, 'normal.b'>
assertType<string>(0 as never as NestedObjectUnionB)
type NestedObjectUnionC = DeepValue<NestedObjectUnionCase, 'normal.c.user.id'>
assertType<string>(0 as never as NestedObjectUnionC)

type NestedNullableObjectUnionCase = {
nullable:
| { a?: number; b?: { c: boolean } | null }
| { b?: { c: string; e: number } }
}
type NestedNullableObjectUnionA = DeepValue<
NestedNullableObjectUnionCase,
'nullable.a'
>
assertType<number | undefined>(0 as never as NestedNullableObjectUnionA)
type NestedNullableObjectUnionB = DeepValue<
NestedNullableObjectUnionCase,
'nullable.b.c'
>
assertType<string | boolean | null | undefined>(
0 as never as NestedNullableObjectUnionB,
)
type NestedNullableObjectUnionC = DeepValue<
NestedNullableObjectUnionCase,
'nullable.b.e'
>
assertType<number | null | undefined>(0 as never as NestedNullableObjectUnionC)

type NestedArrayExample = DeepValue<{ users: User[] }, 'users[0].age'>
assertType<number>(0 as never as NestedArrayExample)

type NestedLooseArrayExample = DeepValue<{ users: User[] }, 'users[number].age'>
assertType<number>(0 as never as NestedLooseArrayExample)

type NestedArrayUnionExample = DeepValue<
{ users: string | User[] },
'users[0].age'
>
assertType<number>(0 as never as NestedArrayUnionExample)

type NestedTupleExample = DeepValue<
{ topUsers: [User, 0, User] },
'topUsers[0].age'
Expand Down