Skip to content

Commit

Permalink
fix: solve deep nested nullable values issue with DeepValue
Browse files Browse the repository at this point in the history
  • Loading branch information
irwinarruda committed Jun 28, 2024
1 parent c3e27c7 commit b5701fd
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 48 deletions.
75 changes: 42 additions & 33 deletions packages/form-core/src/util-types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
// Hack changing Typescript's default get behavior in order to work with unions
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 Nullable<T> = T | null
type IsNullable<T> = [null] extends [T] ? true : false

/**
* @private
*/
Expand Down Expand Up @@ -110,6 +100,16 @@ 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<T> = null extends T ? null : never
type ApplyUndefined<T> = undefined extends T ? undefined : never

/**
* Infer the type of a deeply nested property within an object or an array.
*/
Expand All @@ -118,36 +118,45 @@ 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
? TValue
// Depth for preventing infinite recursion
TDepth extends ReadonlyArray<any> = [],
> = unknown extends TValue // If TValue is any it will recurse forever, this terminates the recursion
? TValue
: TDepth['length'] extends 10
? never
: // Check if we're looking for the property in an array
TValue extends ReadonlyArray<any>
? TAccessor extends `[${infer TBrackets}].${infer TAfter}`
? /*
Extract the first element from the accessor path (`TBrackets`)
and recursively call `DeepValue` with it
*/
DeepValue<DeepValue<TValue, TBrackets>, TAfter>
*/
DeepValue<
DeepValue<TValue, TBrackets, [...TDepth, any]>,
TAfter,
[...TDepth, any]
>
: TAccessor extends `[${infer TBrackets}]`
? DeepValue<TValue, TBrackets>
? DeepValue<TValue, TBrackets, [...TDepth, any]>
: 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<Get<TValue, TAccessor>>
: Get<TValue, TAccessor>
: never
: // Do not allow `TValue` to be anything else
never
: TAccessor extends `${infer TBefore}[${infer TEverythingElse}`
? DeepValue<
DeepValue<TValue, TBefore, [...TDepth, any]>,
`[${TEverythingElse}`,
[...TDepth, any]
>
: TAccessor extends `[${infer TBrackets}]`
? DeepValue<TValue, TBrackets, [...TDepth, any]>
: TAccessor extends `${infer TBefore}.${infer TAfter}`
? DeepValue<
DeepValue<TValue, TBefore, [...TDepth, any]>,
TAfter,
[...TDepth, any]
>
: TAccessor extends string
?
| Get<TValue, TAccessor>
| (ApplyNull<TValue> | ApplyUndefined<TValue>)
: never
2 changes: 1 addition & 1 deletion packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ describe('form api', () => {
const form = new FormApi({
defaultValues: {
person: { firstName: 'firstName' },
} as { person: { firstName: string } | { age: number } },
} as { person?: { firstName: string } | { age: number } | null },
})

const field = new FieldApi({
Expand Down
67 changes: 53 additions & 14 deletions packages/form-core/tests/util-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,51 @@ 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<'hello' | null>(0 as never as NestedNullableKeys)
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<'name' | undefined>(0 as never as NestedNullableObjectCaseOptional)
type NestedNullableObjectCaseMixed = DeepValue<
NestedNullableObjectCase,
'mixed.mainUser'
>
assertType<'name' | null | undefined>(
0 as never as NestedNullableObjectCaseMixed,
)

type NestedUndefinedKeys = DeepValue<
{
meta: { mainUser: 'hello' } | undefined
},
'meta.mainUser'
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<'hello'>(0 as never as NestedUndefinedKeys)
assertType<'name' | null | undefined>(0 as never as DoubleNestedNullableObjectB)

type NestedObjectUnionCase = {
normal:
Expand All @@ -118,7 +148,9 @@ type NestedObjectUnionC = DeepValue<NestedObjectUnionCase, 'normal.c.user.id'>
assertType<string>(0 as never as NestedObjectUnionC)

type NestedNullableObjectUnionCase = {
nullable: { a?: number; b?: { c: boolean } | null }
nullable:
| { a?: number; b?: { c: boolean } | null }
| { b?: { c: string; e: number } }
}
type NestedNullableObjectUnionA = DeepValue<
NestedNullableObjectUnionCase,
Expand All @@ -129,7 +161,14 @@ type NestedNullableObjectUnionB = DeepValue<
NestedNullableObjectUnionCase,
'nullable.b.c'
>
assertType<boolean | null>(0 as never as NestedNullableObjectUnionB)
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)
Expand Down

0 comments on commit b5701fd

Please sign in to comment.