Skip to content

Commit ff9cf14

Browse files
authored
fix: robustify form helper types (#14463)
* WIP We need to make the result nonnullable, because else the optional properties make the resulting type `X | undefined` which when turned into an intersection results in the never type. * proper fix * fix arrays * regenerate
1 parent 8826b0d commit ff9cf14

File tree

4 files changed

+209
-76
lines changed

4 files changed

+209
-76
lines changed

.changeset/funny-onions-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: robustify form helper types

packages/kit/src/exports/public.d.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,38 +1810,40 @@ export interface Snapshot<T = any> {
18101810
restore: (snapshot: T) => void;
18111811
}
18121812

1813-
// If T is unknown or RemoteFormInput, the types below will recurse indefinitely and create giant unions that TS can't handle
1814-
type WillRecurseIndefinitely<T> = unknown extends T
1815-
? true
1816-
: RemoteFormInput extends T
1817-
? true
1818-
: false;
1813+
// If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle
1814+
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
18191815

18201816
// Helper type to convert union to intersection
18211817
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
18221818
? I
18231819
: never;
18241820

1825-
type FlattenInput<T, Prefix extends string> =
1826-
WillRecurseIndefinitely<T> extends true
1821+
type FlattenInput<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1822+
? { [P in Prefix]: string }
1823+
: WillRecurseIndefinitely<T> extends true
18271824
? { [key: string]: string }
18281825
: T extends Array<infer U>
18291826
? U extends string | File
18301827
? { [P in Prefix]: string[] }
18311828
: FlattenInput<U, `${Prefix}[${number}]`>
18321829
: T extends File
18331830
? { [P in Prefix]: string }
1834-
: T extends object
1835-
? {
1836-
[K in keyof T]: FlattenInput<
1837-
T[K],
1838-
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1839-
>;
1840-
}[keyof T]
1841-
: { [P in Prefix]: string };
1842-
1843-
type FlattenIssues<T, Prefix extends string> =
1844-
WillRecurseIndefinitely<T> extends true
1831+
: {
1832+
// Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1833+
[K in keyof Required<T>]: FlattenInput<
1834+
T[K],
1835+
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1836+
>;
1837+
}[keyof T];
1838+
1839+
type FlattenIssues<T, Prefix extends string> = T extends
1840+
| string
1841+
| number
1842+
| boolean
1843+
| null
1844+
| undefined
1845+
? { [P in Prefix]: RemoteFormIssue[] }
1846+
: WillRecurseIndefinitely<T> extends true
18451847
? { [key: string]: RemoteFormIssue[] }
18461848
: T extends Array<infer U>
18471849
? { [P in Prefix | `${Prefix}[${number}]`]: RemoteFormIssue[] } & FlattenIssues<
@@ -1850,32 +1852,31 @@ type FlattenIssues<T, Prefix extends string> =
18501852
>
18511853
: T extends File
18521854
? { [P in Prefix]: RemoteFormIssue[] }
1853-
: T extends object
1854-
? {
1855-
[K in keyof T]: FlattenIssues<
1856-
T[K],
1857-
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1858-
>;
1859-
}[keyof T]
1860-
: { [P in Prefix]: RemoteFormIssue[] };
1861-
1862-
type FlattenKeys<T, Prefix extends string> =
1863-
WillRecurseIndefinitely<T> extends true
1855+
: {
1856+
// Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1857+
[K in keyof Required<T>]: FlattenIssues<
1858+
T[K],
1859+
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1860+
>;
1861+
}[keyof T];
1862+
1863+
type FlattenKeys<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1864+
? { [P in Prefix]: string }
1865+
: WillRecurseIndefinitely<T> extends true
18641866
? { [key: string]: string }
18651867
: T extends Array<infer U>
18661868
? U extends string | File
18671869
? { [P in `${Prefix}[]`]: string[] }
18681870
: FlattenKeys<U, `${Prefix}[${number}]`>
18691871
: T extends File
18701872
? { [P in Prefix]: string }
1871-
: T extends object
1872-
? {
1873-
[K in keyof T]: FlattenKeys<
1874-
T[K],
1875-
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1876-
>;
1877-
}[keyof T]
1878-
: { [P in Prefix]: string };
1873+
: {
1874+
// Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1875+
[K in keyof Required<T>]: FlattenKeys<
1876+
T[K],
1877+
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1878+
>;
1879+
}[keyof T];
18791880

18801881
export interface RemoteFormInput {
18811882
[key: string]: FormDataEntryValue | FormDataEntryValue[] | RemoteFormInput | RemoteFormInput[];

packages/kit/test/types/remote.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,132 @@ function form_tests() {
175175
);
176176
y;
177177
});
178+
179+
const f2 = form(
180+
null as any as StandardSchemaV1<{ a: string; nested: { prop: string } }>,
181+
(data) => {
182+
data.a === '';
183+
data.nested.prop === '';
184+
// @ts-expect-error
185+
data.nested.nonexistent;
186+
// @ts-expect-error
187+
data.nonexistent;
188+
// @ts-expect-error
189+
data.a === 123;
190+
return { success: true };
191+
}
192+
);
193+
f2.field('a');
194+
f2.field('nested.prop');
195+
// @ts-expect-error
196+
f2.field('nonexistent');
197+
f2.issues!.a;
198+
f2.issues!['nested.prop'];
199+
// @ts-expect-error
200+
f2.issues!.nonexistent;
201+
f2.input!.a = '';
202+
f2.input!['nested.prop'] = '';
203+
// @ts-expect-error
204+
f2.input!.nonexistent = '';
205+
// @ts-expect-error
206+
f2.input!.a = 123;
207+
208+
// all schema properties optional
209+
const f3 = form(
210+
null as any as StandardSchemaV1<{ a?: string; nested?: { prop?: string } }>,
211+
(data) => {
212+
data.a === '';
213+
data.nested?.prop === '';
214+
// @ts-expect-error
215+
data.nested.prop === '';
216+
// @ts-expect-error
217+
data.nested.nonexistent;
218+
// @ts-expect-error
219+
data.nonexistent;
220+
// @ts-expect-error
221+
data.a === 123;
222+
return { success: true };
223+
}
224+
);
225+
f3.field('a');
226+
f3.field('nested.prop');
227+
// @ts-expect-error
228+
f3.field('nonexistent');
229+
f3.issues!.a;
230+
f3.issues!['nested.prop'];
231+
// @ts-expect-error
232+
f3.issues!.nonexistent;
233+
f3.input!.a = '';
234+
f3.input!['nested.prop'] = '';
235+
// @ts-expect-error
236+
f3.input!.nonexistent = '';
237+
// @ts-expect-error
238+
f3.input!.a = 123;
239+
240+
// index signature schema
241+
const f4 = form(null as any as StandardSchemaV1<Record<string, any>>, (data) => {
242+
data.a === '';
243+
data.nested?.prop === '';
244+
return { success: true };
245+
});
246+
f4.field('a');
247+
f4.field('nested.prop');
248+
f4.issues!.a;
249+
f4.issues!['nested.prop'];
250+
f4.input!.a = '';
251+
f4.input!['nested.prop'] = '';
252+
// @ts-expect-error
253+
f4.input!.a = 123;
254+
255+
// schema with union types
256+
const f5 = form(null as any as StandardSchemaV1<{ foo: 'a' | 'b'; bar: 'c' | 'd' }>, (data) => {
257+
data.foo === 'a';
258+
data.bar === 'c';
259+
// @ts-expect-error
260+
data.foo === 'e';
261+
return { success: true };
262+
});
263+
f5.field('foo');
264+
// @ts-expect-error
265+
f5.field('nonexistent');
266+
f5.issues!.foo;
267+
f5.issues!.bar;
268+
// @ts-expect-error
269+
f5.issues!.nonexistent;
270+
f5.input!.foo = 'a';
271+
// @ts-expect-error
272+
f5.input!.foo = 123;
273+
274+
// schema with arrays
275+
const f6 = form(
276+
null as any as StandardSchemaV1<{ array: Array<{ array: string[]; prop: string }> }>,
277+
(data) => {
278+
data.array[0].prop === 'a';
279+
data.array[0].array[0] === 'a';
280+
// @ts-expect-error
281+
data.array[0].array[0] === 1;
282+
return { success: true };
283+
}
284+
);
285+
f6.field('array[0].prop');
286+
f6.field('array[0].array[]');
287+
// @ts-expect-error
288+
f6.field('array[0].array');
289+
f6.issues!.array;
290+
f6.issues!['array[0].prop'];
291+
f6.issues!['array[0].array'];
292+
// @ts-expect-error
293+
f6.issues!['array[0].array[]'];
294+
// @ts-expect-error
295+
f6.issues!.nonexistent;
296+
f6.input!['array[0].prop'] = '';
297+
f6.input!['array[0].array'] = [''];
298+
// @ts-expect-error
299+
f6.input!['array[0].array'] = '';
300+
// @ts-expect-error
301+
f6.input!['array[0].array[]'] = [''];
302+
// @ts-expect-error
303+
f6.input!['array[0].prop'] = 123;
178304
}
179305
form_tests();
180306

packages/kit/types/index.d.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,38 +1786,40 @@ declare module '@sveltejs/kit' {
17861786
restore: (snapshot: T) => void;
17871787
}
17881788

1789-
// If T is unknown or RemoteFormInput, the types below will recurse indefinitely and create giant unions that TS can't handle
1790-
type WillRecurseIndefinitely<T> = unknown extends T
1791-
? true
1792-
: RemoteFormInput extends T
1793-
? true
1794-
: false;
1789+
// If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle
1790+
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
17951791

17961792
// Helper type to convert union to intersection
17971793
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
17981794
? I
17991795
: never;
18001796

1801-
type FlattenInput<T, Prefix extends string> =
1802-
WillRecurseIndefinitely<T> extends true
1797+
type FlattenInput<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1798+
? { [P in Prefix]: string }
1799+
: WillRecurseIndefinitely<T> extends true
18031800
? { [key: string]: string }
18041801
: T extends Array<infer U>
18051802
? U extends string | File
18061803
? { [P in Prefix]: string[] }
18071804
: FlattenInput<U, `${Prefix}[${number}]`>
18081805
: T extends File
18091806
? { [P in Prefix]: string }
1810-
: T extends object
1811-
? {
1812-
[K in keyof T]: FlattenInput<
1813-
T[K],
1814-
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1815-
>;
1816-
}[keyof T]
1817-
: { [P in Prefix]: string };
1818-
1819-
type FlattenIssues<T, Prefix extends string> =
1820-
WillRecurseIndefinitely<T> extends true
1807+
: {
1808+
// Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1809+
[K in keyof Required<T>]: FlattenInput<
1810+
T[K],
1811+
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1812+
>;
1813+
}[keyof T];
1814+
1815+
type FlattenIssues<T, Prefix extends string> = T extends
1816+
| string
1817+
| number
1818+
| boolean
1819+
| null
1820+
| undefined
1821+
? { [P in Prefix]: RemoteFormIssue[] }
1822+
: WillRecurseIndefinitely<T> extends true
18211823
? { [key: string]: RemoteFormIssue[] }
18221824
: T extends Array<infer U>
18231825
? { [P in Prefix | `${Prefix}[${number}]`]: RemoteFormIssue[] } & FlattenIssues<
@@ -1826,32 +1828,31 @@ declare module '@sveltejs/kit' {
18261828
>
18271829
: T extends File
18281830
? { [P in Prefix]: RemoteFormIssue[] }
1829-
: T extends object
1830-
? {
1831-
[K in keyof T]: FlattenIssues<
1832-
T[K],
1833-
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1834-
>;
1835-
}[keyof T]
1836-
: { [P in Prefix]: RemoteFormIssue[] };
1837-
1838-
type FlattenKeys<T, Prefix extends string> =
1839-
WillRecurseIndefinitely<T> extends true
1831+
: {
1832+
// Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1833+
[K in keyof Required<T>]: FlattenIssues<
1834+
T[K],
1835+
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1836+
>;
1837+
}[keyof T];
1838+
1839+
type FlattenKeys<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1840+
? { [P in Prefix]: string }
1841+
: WillRecurseIndefinitely<T> extends true
18401842
? { [key: string]: string }
18411843
: T extends Array<infer U>
18421844
? U extends string | File
18431845
? { [P in `${Prefix}[]`]: string[] }
18441846
: FlattenKeys<U, `${Prefix}[${number}]`>
18451847
: T extends File
18461848
? { [P in Prefix]: string }
1847-
: T extends object
1848-
? {
1849-
[K in keyof T]: FlattenKeys<
1850-
T[K],
1851-
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1852-
>;
1853-
}[keyof T]
1854-
: { [P in Prefix]: string };
1849+
: {
1850+
// Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1851+
[K in keyof Required<T>]: FlattenKeys<
1852+
T[K],
1853+
Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1854+
>;
1855+
}[keyof T];
18551856

18561857
export interface RemoteFormInput {
18571858
[key: string]: FormDataEntryValue | FormDataEntryValue[] | RemoteFormInput | RemoteFormInput[];

0 commit comments

Comments
 (0)