From 18ba70dbc26ccd5a396e289372ed8e4082aa7a17 Mon Sep 17 00:00:00 2001 From: irwinarruda Date: Thu, 30 May 2024 00:58:51 -0300 Subject: [PATCH 1/3] fix(core): add support to union types for `DeepValue` --- packages/form-core/src/tests/FormApi.spec.ts | 24 +++++++++++ .../form-core/src/tests/util-types.test-d.ts | 42 +++++++++++++++++++ packages/form-core/src/util-types.ts | 11 ++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/form-core/src/tests/FormApi.spec.ts b/packages/form-core/src/tests/FormApi.spec.ts index c9f402b6f..68ce1ea87 100644 --- a/packages/form-core/src/tests/FormApi.spec.ts +++ b/packages/form-core/src/tests/FormApi.spec.ts @@ -1096,6 +1096,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 } }, + }) + + 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: { diff --git a/packages/form-core/src/tests/util-types.test-d.ts b/packages/form-core/src/tests/util-types.test-d.ts index ae07663a2..94cacaf25 100644 --- a/packages/form-core/src/tests/util-types.test-d.ts +++ b/packages/form-core/src/tests/util-types.test-d.ts @@ -83,12 +83,54 @@ type NestedNullableKeys = DeepValue< > assertType<'hello' | null>(0 as never as NestedNullableKeys) +type NestedUndefinedKeys = DeepValue< + { + meta: { mainUser: 'hello' } | undefined + }, + 'meta.mainUser' +> +assertType<'hello'>(0 as never as NestedUndefinedKeys) + +type NestedObjectUnionCase = { + normal: + | { a: User } + | { a: string } + | { b: string } + | { c: { user: User } | { user: number } } +} +type NestedObjectUnionA = DeepValue +assertType(0 as never as NestedObjectUnionA) +type NestedObjectUnionB = DeepValue +assertType(0 as never as NestedObjectUnionB) +type NestedObjectUnionC = DeepValue +assertType(0 as never as NestedObjectUnionC) + +type NestedNullableObjectUnionCase = { + nullable: { a?: number; b?: { c: boolean } | null } +} +type NestedNullableObjectUnionA = DeepValue< + NestedNullableObjectUnionCase, + 'nullable.a' +> +assertType(0 as never as NestedNullableObjectUnionA) +type NestedNullableObjectUnionB = DeepValue< + NestedNullableObjectUnionCase, + 'nullable.b.c' +> +assertType(0 as never as NestedNullableObjectUnionB) + type NestedArrayExample = DeepValue<{ users: User[] }, 'users[0].age'> assertType(0 as never as NestedArrayExample) type NestedLooseArrayExample = DeepValue<{ users: User[] }, 'users[number].age'> assertType(0 as never as NestedLooseArrayExample) +type NestedArrayUnionExample = DeepValue< + { users: string | User[] }, + 'users[0].age' +> +assertType(0 as never as NestedArrayUnionExample) + type NestedTupleExample = DeepValue< { topUsers: [User, 0, User] }, 'topUsers[0].age' diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index 29f238527..23f896480 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -1,3 +1,10 @@ +// Hack changing Typescript's default get behavior in order to work with unions +type Get = T extends { [Key in K]: infer V } + ? V + : T extends { [Key in K]?: infer W } + ? W | undefined + : never + type Nullable = T | null type IsNullable = [null] extends [T] ? true : false @@ -127,8 +134,8 @@ export type DeepValue< ? DeepValue, TAfter> : TAccessor extends string ? TNullable extends true - ? Nullable - : TValue[TAccessor] + ? Nullable> + : Get : never : // Do not allow `TValue` to be anything else never From b5701fd05bdc7936c676be00e1f8f48276ffab93 Mon Sep 17 00:00:00 2001 From: irwinarruda Date: Fri, 28 Jun 2024 00:28:04 -0300 Subject: [PATCH 2/3] fix: solve deep nested nullable values issue with `DeepValue` --- packages/form-core/src/util-types.ts | 75 +++++++++++-------- packages/form-core/tests/FormApi.spec.ts | 2 +- packages/form-core/tests/util-types.test-d.ts | 67 +++++++++++++---- 3 files changed, 96 insertions(+), 48 deletions(-) diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index 277f90011..f2a0ec04e 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -1,13 +1,3 @@ -// Hack changing Typescript's default get behavior in order to work with unions -type Get = T extends { [Key in K]: infer V } - ? V - : T extends { [Key in K]?: infer W } - ? W | undefined - : never - -type Nullable = T | null -type IsNullable = [null] extends [T] ? true : false - /** * @private */ @@ -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 extends { [Key in K]: infer V } + ? V + : T extends { [Key in K]?: infer W } + ? W | undefined + : never + +type ApplyNull = null extends T ? null : never +type ApplyUndefined = undefined extends T ? undefined : never + /** * Infer the type of a deeply nested property within an object or an array. */ @@ -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, -> = - // If TValue is any it will recurse forever, this terminates the recursion - unknown extends TValue - ? TValue + // Depth for preventing infinite recursion + TDepth extends ReadonlyArray = [], +> = 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 ? TAccessor extends `[${infer TBrackets}].${infer TAfter}` ? /* Extract the first element from the accessor path (`TBrackets`) and recursively call `DeepValue` with it - */ - DeepValue, TAfter> + */ + DeepValue< + DeepValue, + TAfter, + [...TDepth, any] + > : TAccessor extends `[${infer TBrackets}]` - ? DeepValue + ? DeepValue : TAccessor extends keyof TValue ? TValue[TAccessor] : TValue[TAccessor & number] - : // Check if we're looking for the property in an object - TValue extends Record - ? TAccessor extends `${infer TBefore}[${infer TEverythingElse}` - ? DeepValue, `[${TEverythingElse}`> - : TAccessor extends `[${infer TBrackets}]` - ? DeepValue - : TAccessor extends `${infer TBefore}.${infer TAfter}` - ? DeepValue, TAfter> - : TAccessor extends string - ? TNullable extends true - ? Nullable> - : Get - : never - : // Do not allow `TValue` to be anything else - never + : TAccessor extends `${infer TBefore}[${infer TEverythingElse}` + ? DeepValue< + DeepValue, + `[${TEverythingElse}`, + [...TDepth, any] + > + : TAccessor extends `[${infer TBrackets}]` + ? DeepValue + : TAccessor extends `${infer TBefore}.${infer TAfter}` + ? DeepValue< + DeepValue, + TAfter, + [...TDepth, any] + > + : TAccessor extends string + ? + | Get + | (ApplyNull | ApplyUndefined) + : never diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index b7ea1df71..80794239d 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -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({ diff --git a/packages/form-core/tests/util-types.test-d.ts b/packages/form-core/tests/util-types.test-d.ts index 56d04d9ac..d0cf538cb 100644 --- a/packages/form-core/tests/util-types.test-d.ts +++ b/packages/form-core/tests/util-types.test-d.ts @@ -87,21 +87,51 @@ type NestedKeysExample = DeepValue< > assertType(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: @@ -118,7 +148,9 @@ type NestedObjectUnionC = DeepValue assertType(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, @@ -129,7 +161,14 @@ type NestedNullableObjectUnionB = DeepValue< NestedNullableObjectUnionCase, 'nullable.b.c' > -assertType(0 as never as NestedNullableObjectUnionB) +assertType( + 0 as never as NestedNullableObjectUnionB, +) +type NestedNullableObjectUnionC = DeepValue< + NestedNullableObjectUnionCase, + 'nullable.b.e' +> +assertType(0 as never as NestedNullableObjectUnionC) type NestedArrayExample = DeepValue<{ users: User[] }, 'users[0].age'> assertType(0 as never as NestedArrayExample) From f4bfd179ae887de20f4b251e47e01543a7b3596c Mon Sep 17 00:00:00 2001 From: irwinarruda Date: Sat, 29 Jun 2024 15:22:00 -0300 Subject: [PATCH 3/3] fix: remove depth arg from `DeepValue` --- packages/form-core/src/util-types.ts | 48 +++++++++++----------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index f2a0ec04e..cb8fd1925 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -107,8 +107,12 @@ type Get = T extends { [Key in K]: infer V } ? W | undefined : never -type ApplyNull = null extends T ? null : never -type ApplyUndefined = undefined extends T ? undefined : never +type ApplyNull = null extends T ? (null extends C ? C : C | null) : C +type ApplyUndefined = undefined extends T + ? undefined extends C + ? C + : C | undefined + : C /** * Infer the type of a deeply nested property within an object or an array. @@ -118,45 +122,29 @@ export type DeepValue< TValue, // A string representing the path of the property we're trying to access TAccessor, - // Depth for preventing infinite recursion - TDepth extends ReadonlyArray = [], -> = unknown extends TValue // If TValue is any it will recurse forever, this terminates the recursion - ? TValue - : TDepth['length'] extends 10 - ? never +> = + // If TValue is any it will recurse forever, this terminates the recursion + unknown extends TValue + ? TValue : // Check if we're looking for the property in an array TValue extends ReadonlyArray ? TAccessor extends `[${infer TBrackets}].${infer TAfter}` ? /* Extract the first element from the accessor path (`TBrackets`) and recursively call `DeepValue` with it - */ - DeepValue< - DeepValue, - TAfter, - [...TDepth, any] - > + */ + DeepValue, TAfter> : TAccessor extends `[${infer TBrackets}]` - ? DeepValue + ? DeepValue : TAccessor extends keyof TValue ? TValue[TAccessor] : TValue[TAccessor & number] : TAccessor extends `${infer TBefore}[${infer TEverythingElse}` - ? DeepValue< - DeepValue, - `[${TEverythingElse}`, - [...TDepth, any] - > + ? DeepValue, `[${TEverythingElse}`> : TAccessor extends `[${infer TBrackets}]` - ? DeepValue + ? DeepValue : TAccessor extends `${infer TBefore}.${infer TAfter}` - ? DeepValue< - DeepValue, - TAfter, - [...TDepth, any] - > - : TAccessor extends string - ? - | Get - | (ApplyNull | ApplyUndefined) + ? DeepValue, TAfter> + : TAccessor extends `${infer TKey}` + ? ApplyUndefined, TValue>, TValue> : never