Skip to content

Commit 08c9bed

Browse files
fix(form-core): Only allow array deep keys in array methods (#1495)
* add DeepKeysOfType util type This type will help with a lens API for forms so that only names leading to the subset data are allowed. * fix(form-core): only allow array deep keys in array methods --------- Co-authored-by: Leonardo Montini <[email protected]>
1 parent a44b8ff commit 08c9bed

File tree

4 files changed

+179
-29
lines changed

4 files changed

+179
-29
lines changed

packages/form-core/src/FormApi.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import type {
3737
ValidationErrorMap,
3838
ValidationErrorMapKeys,
3939
} from './types'
40-
import type { DeepKeys, DeepValue } from './util-types'
40+
import type { DeepKeys, DeepKeysOfType, DeepValue } from './util-types'
4141
import type { Updater } from './utils'
4242

4343
/**
@@ -1309,7 +1309,9 @@ export class FormApi<
13091309
/**
13101310
* Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type.
13111311
*/
1312-
validateArrayFieldsStartingFrom = async <TField extends DeepKeys<TFormData>>(
1312+
validateArrayFieldsStartingFrom = async <
1313+
TField extends DeepKeysOfType<TFormData, any[]>,
1314+
>(
13131315
field: TField,
13141316
index: number,
13151317
cause: ValidationCause,
@@ -1959,7 +1961,7 @@ export class FormApi<
19591961
/**
19601962
* Pushes a value into an array field.
19611963
*/
1962-
pushFieldValue = <TField extends DeepKeys<TFormData>>(
1964+
pushFieldValue = <TField extends DeepKeysOfType<TFormData, any[]>>(
19631965
field: TField,
19641966
value: DeepValue<TFormData, TField> extends any[]
19651967
? DeepValue<TFormData, TField>[number]
@@ -1974,7 +1976,7 @@ export class FormApi<
19741976
this.validateField(field, 'change')
19751977
}
19761978

1977-
insertFieldValue = async <TField extends DeepKeys<TFormData>>(
1979+
insertFieldValue = async <TField extends DeepKeysOfType<TFormData, any[]>>(
19781980
field: TField,
19791981
index: number,
19801982
value: DeepValue<TFormData, TField> extends any[]
@@ -2006,7 +2008,7 @@ export class FormApi<
20062008
/**
20072009
* Replaces a value into an array field at the specified index.
20082010
*/
2009-
replaceFieldValue = async <TField extends DeepKeys<TFormData>>(
2011+
replaceFieldValue = async <TField extends DeepKeysOfType<TFormData, any[]>>(
20102012
field: TField,
20112013
index: number,
20122014
value: DeepValue<TFormData, TField> extends any[]
@@ -2032,7 +2034,7 @@ export class FormApi<
20322034
/**
20332035
* Removes a value from an array field at the specified index.
20342036
*/
2035-
removeFieldValue = async <TField extends DeepKeys<TFormData>>(
2037+
removeFieldValue = async <TField extends DeepKeysOfType<TFormData, any[]>>(
20362038
field: TField,
20372039
index: number,
20382040
opts?: UpdateMetaOptions,
@@ -2069,7 +2071,7 @@ export class FormApi<
20692071
/**
20702072
* Swaps the values at the specified indices within an array field.
20712073
*/
2072-
swapFieldValues = <TField extends DeepKeys<TFormData>>(
2074+
swapFieldValues = <TField extends DeepKeysOfType<TFormData, any[]>>(
20732075
field: TField,
20742076
index1: number,
20752077
index2: number,
@@ -2098,7 +2100,7 @@ export class FormApi<
20982100
/**
20992101
* Moves the value at the first specified index to the second specified index within an array field.
21002102
*/
2101-
moveFieldValues = <TField extends DeepKeys<TFormData>>(
2103+
moveFieldValues = <TField extends DeepKeysOfType<TFormData, any[]>>(
21022104
field: TField,
21032105
index1: number,
21042106
index2: number,

packages/form-core/src/util-types.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ type Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch
1919
*/
2020
export type Narrow<A> = Try<A, [], NarrowRaw<A>>
2121

22-
export interface AnyDeepKeyAndValue {
23-
key: string
24-
value: any
22+
export interface AnyDeepKeyAndValue<
23+
K extends string = string,
24+
V extends any = any,
25+
> {
26+
key: K
27+
value: V
2528
}
2629

2730
export type ArrayAccessor<TParent extends AnyDeepKeyAndValue> =
@@ -166,3 +169,11 @@ export type DeepValue<TValue, TAccessor> = unknown extends TValue
166169
: TAccessor extends DeepKeys<TValue>
167170
? DeepRecord<TValue>[TAccessor]
168171
: never
172+
173+
/**
174+
* The keys of an object or array, deeply nested and only with a value of TValue
175+
*/
176+
export type DeepKeysOfType<TData, TValue> = Extract<
177+
DeepKeysAndValues<TData>,
178+
AnyDeepKeyAndValue<string, TValue>
179+
>['key']

packages/form-core/tests/FormApi.test-d.ts

+72
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expectTypeOf, it } from 'vitest'
22
import { z } from 'zod'
33
import { FormApi } from '../src'
44
import type {
5+
DeepKeys,
56
GlobalFormValidationError,
67
StandardSchemaV1Issue,
78
ValidationError,
@@ -201,3 +202,74 @@ it('should not allow setting manual errors if no validator is specified', () =>
201202
onServer: undefined
202203
}>
203204
})
205+
206+
it('should only allow array fields for array-specific methods', () => {
207+
type FormValues = {
208+
name: string
209+
age: number
210+
startDate: Date
211+
title: string | null | undefined
212+
relatives: { name: string }[]
213+
counts: (number | null | undefined)[]
214+
}
215+
216+
const defaultValues: FormValues = {
217+
name: '',
218+
age: 0,
219+
startDate: new Date(),
220+
title: null,
221+
relatives: [{ name: '' }],
222+
counts: [5, null, undefined, 3],
223+
}
224+
225+
const form = new FormApi({
226+
defaultValues,
227+
})
228+
form.mount()
229+
230+
type AllKeys = DeepKeys<FormValues>
231+
type OnlyArrayKeys = Extract<AllKeys, 'counts' | 'relatives'>
232+
type RandomKeys = Extract<AllKeys, 'counts' | 'relatives' | 'title'>
233+
234+
const push1 = form.pushFieldValue<OnlyArrayKeys>
235+
// @ts-expect-error too wide!
236+
const push2 = form.pushFieldValue<AllKeys>
237+
// @ts-expect-error too wide!
238+
const push3 = form.pushFieldValue<RandomKeys>
239+
240+
const insert1 = form.insertFieldValue<OnlyArrayKeys>
241+
// @ts-expect-error too wide!
242+
const insert2 = form.insertFieldValue<AllKeys>
243+
// @ts-expect-error too wide!
244+
const insert3 = form.insertFieldValue<RandomKeys>
245+
246+
const replace1 = form.replaceFieldValue<OnlyArrayKeys>
247+
// @ts-expect-error too wide!
248+
const replace2 = form.replaceFieldValue<AllKeys>
249+
// @ts-expect-error too wide!
250+
const replace3 = form.replaceFieldValue<RandomKeys>
251+
252+
const remove1 = form.removeFieldValue<OnlyArrayKeys>
253+
// @ts-expect-error too wide!
254+
const remove2 = form.removeFieldValue<AllKeys>
255+
// @ts-expect-error too wide!
256+
const remove3 = form.removeFieldValue<RandomKeys>
257+
258+
const swap1 = form.swapFieldValues<OnlyArrayKeys>
259+
// @ts-expect-error too wide!
260+
const swap2 = form.swapFieldValues<AllKeys>
261+
// @ts-expect-error too wide!
262+
const swap3 = form.swapFieldValues<RandomKeys>
263+
264+
const move1 = form.moveFieldValues<OnlyArrayKeys>
265+
// @ts-expect-error too wide!
266+
const move2 = form.moveFieldValues<AllKeys>
267+
// @ts-expect-error too wide!
268+
const move3 = form.moveFieldValues<RandomKeys>
269+
270+
const validate1 = form.validateArrayFieldsStartingFrom<OnlyArrayKeys>
271+
// @ts-expect-error too wide!
272+
const validate2 = form.validateArrayFieldsStartingFrom<AllKeys>
273+
// @ts-expect-error too wide!
274+
const validate3 = form.validateArrayFieldsStartingFrom<RandomKeys>
275+
})

packages/form-core/tests/util-types.test-d.ts

+83-18
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expectTypeOf } from 'vitest'
2-
import type { DeepKeys, DeepValue } from '../src/index'
2+
import type { DeepKeys, DeepKeysOfType, DeepValue } from '../src/index'
33

44
/**
55
* Properly recognizes that `0` is not an object and should not have subkeys
66
*/
7-
type TupleSupport = DeepKeys<{ topUsers: [User, 0, User] }>
8-
expectTypeOf(0 as never as TupleSupport).toEqualTypeOf<
7+
type TupleSupport = { topUsers: [User, 0, User] }
8+
expectTypeOf(0 as never as DeepKeys<TupleSupport>).toEqualTypeOf<
99
| 'topUsers'
1010
| 'topUsers[0]'
1111
| 'topUsers[0].name'
@@ -17,50 +17,95 @@ expectTypeOf(0 as never as TupleSupport).toEqualTypeOf<
1717
| 'topUsers[2].id'
1818
| 'topUsers[2].age'
1919
>()
20+
expectTypeOf(0 as never as DeepKeysOfType<TupleSupport, number>).toEqualTypeOf<
21+
'topUsers[0].age' | 'topUsers[1]' | 'topUsers[2].age'
22+
>()
23+
expectTypeOf(0 as never as DeepKeysOfType<TupleSupport, string>).toEqualTypeOf<
24+
'topUsers[0].name' | 'topUsers[0].id' | 'topUsers[2].name' | 'topUsers[2].id'
25+
>()
26+
expectTypeOf(0 as never as DeepKeysOfType<TupleSupport, User>).toEqualTypeOf<
27+
'topUsers[0]' | 'topUsers[2]'
28+
>()
29+
expectTypeOf(
30+
0 as never as DeepKeysOfType<TupleSupport, Date>,
31+
).toEqualTypeOf<never>()
2032

2133
/**
2234
* Properly recognizes that a normal number index won't cut it and should be `[number]` prefixed instead
2335
*/
24-
type ArraySupport = DeepKeys<{ users: User[] }>
25-
expectTypeOf(0 as never as ArraySupport).toEqualTypeOf<
36+
type ArraySupport = { users: User[] }
37+
expectTypeOf(0 as never as DeepKeys<ArraySupport>).toEqualTypeOf<
2638
| 'users'
2739
| `users[${number}]`
2840
| `users[${number}].name`
2941
| `users[${number}].id`
3042
| `users[${number}].age`
3143
>()
44+
expectTypeOf(
45+
0 as never as DeepKeysOfType<ArraySupport, number>,
46+
).toEqualTypeOf<`users[${number}].age`>()
47+
expectTypeOf(0 as never as DeepKeysOfType<ArraySupport, string>).toEqualTypeOf<
48+
`users[${number}].name` | `users[${number}].id`
49+
>()
50+
expectTypeOf(
51+
0 as never as DeepKeysOfType<ArraySupport, User>,
52+
).toEqualTypeOf<`users[${number}]`>()
53+
expectTypeOf(
54+
0 as never as DeepKeysOfType<ArraySupport, Date>,
55+
).toEqualTypeOf<never>()
3256

3357
/**
3458
* Properly handles deep object nesting like so:
3559
*/
36-
type NestedSupport = DeepKeys<{ meta: { mainUser: User } }>
37-
expectTypeOf(0 as never as NestedSupport).toEqualTypeOf<
60+
type NestedSupport = { meta: { mainUser: User } }
61+
expectTypeOf(0 as never as DeepKeys<NestedSupport>).toEqualTypeOf<
3862
| 'meta'
3963
| 'meta.mainUser'
4064
| 'meta.mainUser.name'
4165
| 'meta.mainUser.id'
4266
| 'meta.mainUser.age'
4367
>()
68+
expectTypeOf(
69+
0 as never as DeepKeysOfType<NestedSupport, number>,
70+
).toEqualTypeOf<`meta.mainUser.age`>()
71+
expectTypeOf(0 as never as DeepKeysOfType<NestedSupport, string>).toEqualTypeOf<
72+
`meta.mainUser.name` | `meta.mainUser.id`
73+
>()
74+
expectTypeOf(
75+
0 as never as DeepKeysOfType<NestedSupport, User>,
76+
).toEqualTypeOf<`meta.mainUser`>()
77+
expectTypeOf(
78+
0 as never as DeepKeysOfType<NestedSupport, Date>,
79+
).toEqualTypeOf<never>()
4480

4581
/**
4682
* Properly handles deep partial object nesting like so:
4783
*/
48-
type NestedPartialSupport = DeepKeys<{ meta?: { mainUser?: User } }>
49-
expectTypeOf(0 as never as NestedPartialSupport).toEqualTypeOf<
84+
type NestedPartialSupport = { meta?: { mainUser?: User } }
85+
expectTypeOf(0 as never as DeepKeys<NestedPartialSupport>).toEqualTypeOf<
5086
| 'meta'
5187
| 'meta.mainUser'
5288
| 'meta.mainUser.name'
5389
| 'meta.mainUser.id'
5490
| 'meta.mainUser.age'
5591
>()
92+
expectTypeOf(
93+
0 as never as DeepKeysOfType<NestedPartialSupport, number>,
94+
).toEqualTypeOf<never>()
95+
expectTypeOf(
96+
0 as never as DeepKeysOfType<NestedPartialSupport, number | undefined>,
97+
).toEqualTypeOf<'meta.mainUser.age'>()
5698

5799
/**
58100
* Properly handles `object` edgecase nesting like so:
59101
*/
60-
type ObjectNestedEdgecase = DeepKeys<{ meta: { mainUser: object } }>
61-
expectTypeOf(0 as never as ObjectNestedEdgecase).toEqualTypeOf(
102+
type ObjectNestedEdgecase = { meta: { mainUser: object } }
103+
expectTypeOf(0 as never as DeepKeys<ObjectNestedEdgecase>).toEqualTypeOf(
62104
0 as never as 'meta' | 'meta.mainUser' | `meta.mainUser.${string}`,
63105
)
106+
expectTypeOf(
107+
0 as never as DeepKeysOfType<ObjectNestedEdgecase, object>,
108+
).toEqualTypeOf<'meta' | 'meta.mainUser'>()
64109

65110
/**
66111
* Properly handles `object` edgecase like so:
@@ -71,10 +116,13 @@ expectTypeOf(0 as never as ObjectEdgecase).toEqualTypeOf<string>()
71116
/**
72117
* Properly handles `object` edgecase nesting like so:
73118
*/
74-
type UnknownNestedEdgecase = DeepKeys<{ meta: { mainUser: unknown } }>
119+
type UnknownNestedEdgecase = { meta: { mainUser: unknown } }
75120
expectTypeOf(
76121
0 as never as 'meta' | 'meta.mainUser' | `meta.mainUser.${string}`,
77-
).toEqualTypeOf(0 as never as UnknownNestedEdgecase)
122+
).toEqualTypeOf(0 as never as DeepKeys<UnknownNestedEdgecase>)
123+
expectTypeOf(
124+
0 as never as DeepKeysOfType<UnknownNestedEdgecase, object>,
125+
).toEqualTypeOf<'meta'>()
78126

79127
/**
80128
* Properly handles discriminated unions like so:
@@ -83,10 +131,15 @@ type DiscriminatedUnion = { name: string } & (
83131
| { variant: 'foo' }
84132
| { variant: 'bar'; baz: boolean }
85133
)
86-
type DiscriminatedUnionKeys = DeepKeys<DiscriminatedUnion>
87-
expectTypeOf(0 as never as DiscriminatedUnionKeys).toEqualTypeOf<
134+
expectTypeOf(0 as never as DeepKeys<DiscriminatedUnion>).toEqualTypeOf<
88135
'name' | 'variant' | 'baz'
89136
>()
137+
expectTypeOf(
138+
0 as never as DeepKeysOfType<DiscriminatedUnion, string>,
139+
).toEqualTypeOf<'name' | 'variant'>()
140+
expectTypeOf(
141+
0 as never as DeepKeysOfType<DiscriminatedUnion, boolean>,
142+
).toEqualTypeOf<'baz'>()
90143

91144
type DiscriminatedUnionValueShared = DeepValue<DiscriminatedUnion, 'variant'>
92145
expectTypeOf(0 as never as DiscriminatedUnionValueShared).toEqualTypeOf<
@@ -98,10 +151,16 @@ expectTypeOf(
98151
).toEqualTypeOf<boolean>()
99152

100153
/**
101-
* Properly handles `object` edgecase like so:
154+
* Properly handles `unknown` edgecase like so:
102155
*/
103156
type UnknownEdgecase = DeepKeys<unknown>
104157
expectTypeOf(0 as never as UnknownEdgecase).toEqualTypeOf<string>()
158+
expectTypeOf(
159+
0 as never as DeepKeysOfType<unknown, unknown>,
160+
).toEqualTypeOf<string>()
161+
expectTypeOf(
162+
0 as never as DeepKeysOfType<unknown, object>,
163+
).toEqualTypeOf<never>()
105164

106165
type NestedKeysExample = DeepValue<
107166
{ meta: { mainUser: User } },
@@ -364,10 +423,16 @@ type ObjectWithAny = {
364423
}
365424
}
366425

367-
type AnyObjectKeys = DeepKeys<ObjectWithAny>
368-
expectTypeOf(0 as never as AnyObjectKeys).toEqualTypeOf<
426+
expectTypeOf(0 as never as DeepKeys<ObjectWithAny>).toEqualTypeOf<
369427
'a' | 'b' | 'obj' | `a.${string}` | 'obj.c' | `obj.c.${string}` | 'obj.d'
370428
>()
429+
// since any can also be number, It's okay to be included
430+
expectTypeOf(0 as never as DeepKeysOfType<ObjectWithAny, number>).toEqualTypeOf<
431+
'a' | 'b' | 'obj.c' | 'obj.d'
432+
>()
433+
expectTypeOf(0 as never as DeepKeysOfType<ObjectWithAny, string>).toEqualTypeOf<
434+
'a' | 'obj.c'
435+
>()
371436
type AnyObjectExample = DeepValue<ObjectWithAny, 'a'>
372437
expectTypeOf(0 as never as AnyObjectExample).toEqualTypeOf<any>()
373438
type AnyObjectExample2 = DeepValue<ObjectWithAny, 'b'>

0 commit comments

Comments
 (0)