diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index 8a9a107cd..cb8fd1925 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -1,6 +1,3 @@ -type Nullable = T | null -type IsNullable = [null] extends [T] ? true : false - /** * @private */ @@ -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 extends { [Key in K]: infer V } + ? V + : T extends { [Key in K]?: infer W } + ? W | 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. */ @@ -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, > = // If TValue is any it will recurse forever, this terminates the recursion unknown extends TValue @@ -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 - ? 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 - : TValue[TAccessor] - : never - : // Do not allow `TValue` to be anything else - never + : TAccessor extends `${infer TBefore}[${infer TEverythingElse}` + ? DeepValue, `[${TEverythingElse}`> + : TAccessor extends `[${infer TBrackets}]` + ? DeepValue + : TAccessor extends `${infer TBefore}.${infer TAfter}` + ? DeepValue, TAfter> + : TAccessor extends `${infer TKey}` + ? ApplyUndefined, TValue>, TValue> + : never diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 5691a3fa3..80794239d 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -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: { diff --git a/packages/form-core/tests/util-types.test-d.ts b/packages/form-core/tests/util-types.test-d.ts index cf692c845..d0cf538cb 100644 --- a/packages/form-core/tests/util-types.test-d.ts +++ b/packages/form-core/tests/util-types.test-d.ts @@ -87,13 +87,88 @@ 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<'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 +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 } + | { b?: { c: string; e: number } } +} +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 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) @@ -101,6 +176,12 @@ 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'