Skip to content

Commit 96f7972

Browse files
committed
Great improvement of type inference for proxies.
1 parent 81bd968 commit 96f7972

File tree

9 files changed

+75
-52
lines changed

9 files changed

+75
-52
lines changed

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
### Fixed
1717

1818
- `defaults` didn't infer the input type, and didn't generate correct `SuperValidated` data, making `superForm` confused. Also fixed type signature and removed the `jsonSchema` option that wasn't applicable.
19-
- A successful `PageData` result from `invalidateAll` was overwritten by the `ActionData` result.
2019
- Using `goto` in events didn't work when the target page redirected.
2120
- `FormPath` and `FormPathLeaves` didn't handle fields with type `unknown` and `any`.
2221
- Missing boolean fields were valid in strict mode.

src/lib/client/proxies.ts

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
import { derived, get, type Readable, type Updater, type Writable } from 'svelte/store';
23
import type { InputConstraint } from '../index.js';
34
import { SuperFormError } from '$lib/errors.js';
45
import { pathExists, traversePath } from '../traversal.js';
56
import { splitPath, type FormPath, type FormPathLeaves, type FormPathType } from '../stringPath.js';
67
import type { FormPathArrays } from '../stringPath.js';
78
import type { SuperForm, TaintOption } from './index.js';
8-
import type { Prettify } from '$lib/utils.js';
9+
import type { IsAny, Prettify } from '$lib/utils.js';
910

1011
export type ProxyOptions = {
1112
taint?: TaintOption;
1213
};
1314

14-
type FormPaths<T extends Record<string, unknown>> = FormPath<T> | FormPathLeaves<T>;
15+
type FormPaths<T extends Record<string, unknown>, Type = any> =
16+
| FormPath<T, Type>
17+
| FormPathLeaves<T, Type>;
1518

1619
type CorrectProxyType<In, Out, T extends Record<string, unknown>, Path extends FormPaths<T>> =
1720
NonNullable<FormPathType<T, Path>> extends In ? Writable<Out> : never;
1821

22+
type PathType<Type, T, Path extends string> =
23+
IsAny<Type> extends true ? FormPathType<T, Path> : Type;
24+
1925
type DefaultOptions = {
2026
trueStringValue: string;
2127
dateFormat:
@@ -236,27 +242,25 @@ function _stringProxy<T extends Record<string, unknown>, Path extends FormPaths<
236242
};
237243
}
238244

239-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
240245
type ValueErrors = any[];
241246

242247
export type ArrayProxy<T, Path = string, Errors = ValueErrors> = {
243248
path: Path;
244-
values: Writable<T & unknown[]>;
249+
values: Writable<T[] & unknown[]>;
245250
errors: Writable<string[] | undefined>;
246251
valueErrors: Writable<Errors>;
247252
};
248253

249-
export function arrayProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T>>(
250-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
254+
export function arrayProxy<
255+
T extends Record<string, unknown>,
256+
Path extends FormPathArrays<T, ArrType>,
257+
ArrType = any
258+
>(
251259
superForm: SuperForm<T, any>,
252260
path: Path,
253261
options?: { taint?: TaintOption }
254-
): ArrayProxy<FormPathType<T, Path>, Path> {
255-
const formErrors = fieldProxy(
256-
superForm.errors,
257-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
258-
`${path}` as any
259-
);
262+
): ArrayProxy<FormPathType<T, Path> extends (infer U)[] ? U : never, Path> {
263+
const formErrors = fieldProxy(superForm.errors, `${path}` as any);
260264

261265
const onlyFieldErrors = derived<typeof formErrors, ValueErrors>(formErrors, ($errors) => {
262266
const output: ValueErrors = [];
@@ -320,13 +324,10 @@ export function arrayProxy<T extends Record<string, unknown>, Path extends FormP
320324

321325
return {
322326
path,
323-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
324327
values: values as any,
325-
errors: fieldProxy(
326-
superForm.errors,
327-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
328-
`${path}._errors` as any
329-
) as Writable<string[] | undefined>,
328+
errors: fieldProxy(superForm.errors, `${path}._errors` as any) as Writable<
329+
string[] | undefined
330+
>,
330331
valueErrors: fieldErrors
331332
};
332333
}
@@ -339,12 +340,15 @@ export type FormFieldProxy<T, Path = string> = {
339340
tainted: Writable<boolean | undefined>;
340341
};
341342

342-
export function formFieldProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T>>(
343-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
343+
export function formFieldProxy<
344+
T extends Record<string, unknown>,
345+
Path extends FormPathLeaves<T, Type>,
346+
Type = any
347+
>(
344348
superForm: SuperForm<T, any>,
345349
path: Path,
346350
options?: ProxyOptions
347-
): FormFieldProxy<FormPathType<T, Path>, Path> {
351+
): FormFieldProxy<PathType<Type, T, Path>, Path> {
348352
const path2 = splitPath(path);
349353
// Filter out array indices, the constraints structure doesn't contain these.
350354
const constraintsPath = path2.filter((p) => /\D/.test(String(p))).join('.');
@@ -387,7 +391,6 @@ export function formFieldProxy<T extends Record<string, unknown>, Path extends F
387391
return {
388392
path,
389393
value: superFieldProxy(superForm, path, options),
390-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
391394
errors: fieldProxy(superForm.errors, path as any) as unknown as Writable<string[] | undefined>,
392395
constraints: fieldProxy(superForm.constraints, constraintsPath as never) as Writable<
393396
InputConstraint | undefined
@@ -396,10 +399,10 @@ export function formFieldProxy<T extends Record<string, unknown>, Path extends F
396399
};
397400
}
398401

399-
function updateProxyField<T extends Record<string, unknown>, Path extends string>(
402+
function updateProxyField<T extends Record<string, unknown>, Path extends string, Type = any>(
400403
obj: T,
401404
path: (string | number | symbol)[],
402-
updater: Updater<FormPathType<T, Path>>
405+
updater: Updater<PathType<Type, T, Path>>
403406
) {
404407
const output = traversePath(obj, path, ({ parent, key, value }) => {
405408
if (value === undefined) parent[key] = /\D/.test(key) ? {} : [];
@@ -418,11 +421,11 @@ type SuperFieldProxy<T> = {
418421
update(this: void, updater: Updater<T>, options?: { taint?: TaintOption }): void;
419422
};
420423

421-
function superFieldProxy<T extends Record<string, unknown>, Path extends string>(
424+
function superFieldProxy<T extends Record<string, unknown>, Path extends string, Type = any>(
422425
superForm: SuperForm<T>,
423426
path: Path,
424427
baseOptions?: ProxyOptions
425-
): SuperFieldProxy<FormPathType<T, Path>> {
428+
): SuperFieldProxy<PathType<Type, T, Path>> {
426429
const form = superForm.form;
427430
const path2 = splitPath(path);
428431

@@ -436,10 +439,10 @@ function superFieldProxy<T extends Record<string, unknown>, Path extends string>
436439
const unsub = proxy.subscribe(...params);
437440
return () => unsub();
438441
},
439-
update(upd: Updater<FormPathType<T, Path>>, options?: ProxyOptions) {
442+
update(upd: Updater<PathType<Type, T, Path>>, options?: ProxyOptions) {
440443
form.update((data) => updateProxyField(data, path2, upd), options ?? baseOptions);
441444
},
442-
set(value: FormPathType<T, Path>, options?: ProxyOptions) {
445+
set(value: PathType<Type, T, Path>, options?: ProxyOptions) {
443446
form.update((data) => updateProxyField(data, path2, () => value), options ?? baseOptions);
444447
}
445448
};
@@ -462,11 +465,15 @@ function isSuperForm<T extends Record<string, unknown>>(
462465

463466
export type FieldProxy<T> = Writable<T>;
464467

465-
export function fieldProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(
468+
export function fieldProxy<
469+
T extends Record<string, unknown>,
470+
Path extends FormPaths<T, Type>,
471+
Type = any
472+
>(
466473
form: Writable<T> | SuperForm<T, unknown>,
467474
path: Path,
468475
options?: ProxyOptions
469-
): FieldProxy<FormPathType<T, Path>> {
476+
): FieldProxy<PathType<Type, T, Path>> {
470477
const path2 = splitPath(path);
471478

472479
if (isSuperForm(form, options)) {
@@ -483,10 +490,10 @@ export function fieldProxy<T extends Record<string, unknown>, Path extends FormP
483490
const unsub = proxy.subscribe(...params);
484491
return () => unsub();
485492
},
486-
update(upd: Updater<FormPathType<T, Path>>) {
493+
update(upd: Updater<PathType<Type, T, Path>>) {
487494
form.update((data) => updateProxyField(data, path2, upd));
488495
},
489-
set(value: FormPathType<T, Path>) {
496+
set(value: PathType<Type, T, Path>) {
490497
form.update((data) => updateProxyField(data, path2, () => value));
491498
}
492499
};

src/lib/stringPath.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { IsAny } from './utils.js';
2+
13
/* eslint-disable @typescript-eslint/no-explicit-any */
24
export function splitPath(path: string) {
35
return path
@@ -54,9 +56,6 @@ export type FormPathLeavesWithErrors<T extends object, Type = any> = string &
5456
export type FormPathArrays<T extends object, Type = any> = string &
5557
StringPath<T, { filter: 'arrays'; objAppend: never; path: ''; type: Type }>;
5658

57-
// Thanks to https://stackoverflow.com/a/77451367/70894
58-
type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false;
59-
6059
type Concat<
6160
Path extends string,
6261
Next extends string

src/lib/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ export type MaybePromise<T> = T | Promise<T>;
2020

2121
// eslint-disable-next-line @typescript-eslint/ban-types
2222
export type Prettify<T> = T extends object ? { [K in keyof T]: T[K] } : T & {};
23+
24+
// Thanks to https://stackoverflow.com/a/77451367/70894
25+
export type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false;

src/routes/(v1)/proxies/ProxyField.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
format: 'datetime'
3636
});
3737
}
38-
$: boolValue = value as Writable<boolean>;
38+
const boolValue = value as Writable<boolean>;
3939
</script>
4040

4141
<label for={String(field)} class={type === 'checkbox' ? 'flex items-center pt-2 space-x-2' : ''}

src/routes/(v1)/tests/array-component/+page.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { superForm } from '$lib/client/index.js';
2+
import { arrayProxy, superForm } from '$lib/client/index.js';
33
import type { PageData } from './$types.js';
44
import SuperDebug from '$lib/client/SuperDebug.svelte';
55
import AutoComplete from './AutoComplete.svelte';
@@ -24,8 +24,8 @@
2424
{ value: 'DI', label: 'Diadem' }
2525
];
2626
27-
//const { values } = arrayProxy(pageForm, 'sub.tags');
28-
//$values.push('test');
27+
const { values } = arrayProxy(pageForm, 'sub.tags');
28+
console.log($values[0]?.charAt(0));
2929
</script>
3030

3131
{#if taintedForm}<h3>FORM IS TAINTED</h3>{/if}

src/routes/(v2)/v2/array-proxy/ArrayField.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
</script>
55

66
<script lang="ts" generics="T extends Record<string, unknown>, F extends FormPathArrays<T>">
7-
import type { Writable } from 'svelte/store';
8-
import { arrayProxy } from '$lib/index.js';
7+
import { arrayProxy, type ArrayProxy } from '$lib/index.js';
98
import type { SuperForm, FormPathArrays, FormPathType } from '$lib/index.js';
109
1110
export let form: SuperForm<T>;
1211
export let field: F;
1312
export let newValue: FormPathType<T, F> extends (infer S)[] ? S : never;
1413
15-
const { values: v, errors, valueErrors } = arrayProxy(form, field);
16-
const values = v as Writable<(typeof newValue)[]>;
14+
const { values, errors, valueErrors } = arrayProxy(form, field) satisfies ArrayProxy<
15+
typeof newValue
16+
>;
1717
</script>
1818

1919
<div>

src/routes/(v2)/v2/issue-337-checkboxes/CheckBox.svelte

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@
33
</script>
44

55
<script lang="ts" generics="T extends Record<string, unknown>">
6-
import { formFieldProxy, type SuperForm, type FormPathLeaves } from '$lib/index.js';
7-
import type { Writable } from 'svelte/store';
6+
import {
7+
formFieldProxy,
8+
type FormFieldProxy,
9+
type SuperForm,
10+
type FormPathLeaves
11+
} from '$lib/index.js';
812
9-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10-
export let form: SuperForm<T, any>;
11-
export let field: FormPathLeaves<T>;
13+
export let form: SuperForm<T>;
14+
export let field: FormPathLeaves<T, boolean>;
1215
13-
const { value, constraints } = formFieldProxy(form, field);
14-
$: boolValue = value as Writable<boolean>;
16+
const { value, constraints } = formFieldProxy(form, field) satisfies FormFieldProxy<boolean>;
1517
</script>
1618

1719
<input
1820
name={field}
1921
type="checkbox"
2022
class="checkbox"
21-
bind:checked={$boolValue}
23+
bind:checked={$value}
2224
{...$constraints}
2325
{...$$restProps}
2426
/>

src/tests/legacy/paths.test-d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,19 @@ test('FormPath with type narrowing, arrays', () => {
323323
const i2: NameArrays = 'tags';
324324
});
325325

326+
test('FormPath with type narrowing, union', () => {
327+
type NameArrays = FormPath<Obj, string | number>;
328+
329+
const t1: NameArrays = 'name';
330+
const t2: NameArrays = 'points';
331+
const t3: NameArrays = 'tags[3].name';
332+
const t4: NameArrays = 'names[3]';
333+
const t5: NameArrays = 'tags[0].parents[5]';
334+
335+
// @ts-expect-error incorrect path
336+
const i1: NameArrays = 'tags';
337+
});
338+
326339
test('FormPath with unknown type', () => {
327340
const schema = z.object({
328341
name: z.string(),

0 commit comments

Comments
 (0)