Skip to content

Commit 6a9cdaa

Browse files
authored
fix: relax form typings for union types (#15687)
`someField.set(...)` now works on union types; it no longer only accepts the intersection of all unions. Similarly, `someField.someSubProperty` now works on unions, though at the slight cost of potentially accessing a combination of things you shouldn't be able to. But I think that's better than the other workarounds you have to do, and you can always narrow it if you want to (in a much easier way than the other way around). Fixes #14667
1 parent 28c075d commit 6a9cdaa

4 files changed

Lines changed: 75 additions & 6 deletions

File tree

.changeset/solid-seas-know.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: relax form typings for union types

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,6 +1932,15 @@ type RemoteFormFieldMethods<T> = {
19321932
issues(): RemoteFormIssue[] | undefined;
19331933
};
19341934

1935+
// These two types use "T extends unknown ? .. : .." to distribute over unions.
1936+
// Example: if "type T = A | b" then "keyof T" only contains keys that both A and B have, with "KeysOfUnion<T>" we get the keys of both A and B
1937+
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
1938+
type ValueOfUnionKey<T, K extends PropertyKey> = T extends unknown
1939+
? K extends keyof T
1940+
? T[K]
1941+
: never
1942+
: never;
1943+
19351944
export type RemoteFormFieldValue = string | string[] | number | boolean | File | File[];
19361945

19371946
type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
@@ -2004,14 +2013,15 @@ export type RemoteFormFields<T> =
20042013
? RecursiveFormFields
20052014
: NonNullable<T> extends string | number | boolean | File
20062015
? RemoteFormField<NonNullable<T>>
2007-
: T extends string[] | File[]
2016+
: // [T] is used to prevent distributing over union, only the last condition should distribute over unions
2017+
[T] extends [string[] | File[]]
20082018
? RemoteFormField<T> & { [K in number]: RemoteFormField<T[number]> }
2009-
: T extends Array<infer U>
2019+
: [T] extends [Array<infer U>]
20102020
? RemoteFormFieldContainer<T> & {
20112021
[K in number]: RemoteFormFields<U>;
20122022
}
20132023
: RemoteFormFieldContainer<T> & {
2014-
[K in keyof T]-?: RemoteFormFields<T[K]>;
2024+
[K in KeysOfUnion<T>]-?: RemoteFormFields<ValueOfUnionKey<T, K>>;
20152025
};
20162026

20172027
// By breaking this out into its own type, we avoid the TS recursion depth limit

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { query, prerender, command, form, requested } from '$app/server';
22
import { StandardSchemaV1 } from '@standard-schema/spec';
33
import {
44
RemoteForm,
5+
RemoteFormFields,
56
RemoteFormInput,
67
RemotePrerenderFunction,
78
RemoteQueryFunction,
@@ -442,6 +443,49 @@ function form_tests() {
442443
form.fields.allIssues();
443444
}
444445
void f10;
446+
447+
const f11 = form(
448+
null as any as StandardSchemaV1<{
449+
differing: { type: 'a'; propA: string } | { type: 'b'; propB?: string };
450+
}>,
451+
(data) => {
452+
data.differing.type === 'a' || data.differing.type === 'b';
453+
if (data.differing.type === 'a') {
454+
data.differing.propA === '';
455+
// @ts-expect-error
456+
data.differing.propB;
457+
} else {
458+
data.differing.propB === '';
459+
// @ts-expect-error
460+
data.differing.propA;
461+
}
462+
return { success: true };
463+
}
464+
);
465+
f11.fields.differing.issues();
466+
f11.fields.differing.value().type === 'a' || f11.fields.differing.value().type === 'b';
467+
const f11_field = f11.fields.differing.value();
468+
if (f11_field.type === 'a') {
469+
f11_field.propA === '';
470+
// @ts-expect-error
471+
f11_field.propB;
472+
}
473+
f11.fields.differing.propA.value();
474+
f11.fields.differing.propB.issues();
475+
// @ts-expect-error
476+
f11.fields.differing.propC.value();
477+
f11.fields.differing.set({ type: 'a', propA: 'test' });
478+
f11.fields.differing.set({ type: 'b', propB: 'test' });
479+
f11.fields.differing.set({ type: 'b' });
480+
// @ts-expect-error
481+
f11.fields.differing.set({ type: 'b', propA: 'test' });
482+
// @ts-expect-error
483+
f11.fields.differing.set({ type: 'a', propB: 'test' });
484+
// test that you can narrow yourself if you want to
485+
const f11_field2 = f11.fields.differing as RemoteFormFields<{ type: 'a'; propA: string }>;
486+
f11_field2.propA;
487+
// @ts-expect-error
488+
f11_field2.propB;
445489
}
446490
form_tests();
447491

packages/kit/types/index.d.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1906,6 +1906,15 @@ declare module '@sveltejs/kit' {
19061906
issues(): RemoteFormIssue[] | undefined;
19071907
};
19081908

1909+
// These two types use "T extends unknown ? .. : .." to distribute over unions.
1910+
// Example: if "type T = A | b" then "keyof T" only contains keys that both A and B have, with "KeysOfUnion<T>" we get the keys of both A and B
1911+
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
1912+
type ValueOfUnionKey<T, K extends PropertyKey> = T extends unknown
1913+
? K extends keyof T
1914+
? T[K]
1915+
: never
1916+
: never;
1917+
19091918
export type RemoteFormFieldValue = string | string[] | number | boolean | File | File[];
19101919

19111920
type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
@@ -1978,14 +1987,15 @@ declare module '@sveltejs/kit' {
19781987
? RecursiveFormFields
19791988
: NonNullable<T> extends string | number | boolean | File
19801989
? RemoteFormField<NonNullable<T>>
1981-
: T extends string[] | File[]
1990+
: // [T] is used to prevent distributing over union, only the last condition should distribute over unions
1991+
[T] extends [string[] | File[]]
19821992
? RemoteFormField<T> & { [K in number]: RemoteFormField<T[number]> }
1983-
: T extends Array<infer U>
1993+
: [T] extends [Array<infer U>]
19841994
? RemoteFormFieldContainer<T> & {
19851995
[K in number]: RemoteFormFields<U>;
19861996
}
19871997
: RemoteFormFieldContainer<T> & {
1988-
[K in keyof T]-?: RemoteFormFields<T[K]>;
1998+
[K in KeysOfUnion<T>]-?: RemoteFormFields<ValueOfUnionKey<T, K>>;
19891999
};
19902000

19912001
// By breaking this out into its own type, we avoid the TS recursion depth limit

0 commit comments

Comments
 (0)