Skip to content

Commit 90bbe8a

Browse files
committed
formFieldProxy and setError are now using StringPathLeaves.
1 parent a203511 commit 90bbe8a

File tree

7 files changed

+118
-20
lines changed

7 files changed

+118
-20
lines changed

src/index.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,11 @@ test('Nullable values', async () => {
173173
})
174174
.refine((data) => data);
175175

176-
const schema = refinedSchema._def.schema;
177-
178-
const output = defaultValues(schema);
176+
const output = defaultValues(refinedSchema);
179177
expect(output.scopeId).equals(0);
180178
expect(output.name).equals(null);
181179

180+
const schema = refinedSchema._def.schema;
182181
const extended = schema
183182
.extend({
184183
scopeId: schema.shape.scopeId.default(7)

src/lib/client/proxies.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { z, AnyZodObject } from 'zod';
1010
import {
1111
splitPath,
1212
type StringPath,
13+
type StringPathLeaves,
1314
type StringPathType
1415
} from '../stringPath.js';
1516

@@ -218,8 +219,16 @@ export type FieldProxy<
218219

219220
export function formFieldProxy<
220221
T extends AnyZodObject,
221-
Path extends string & StringPath<z.infer<T>>
222-
>(form: SuperForm<UnwrapEffects<T>, unknown>, path: Path) {
222+
Path extends string & StringPathLeaves<z.infer<T>>
223+
>(
224+
form: SuperForm<UnwrapEffects<T>, unknown>,
225+
path: Path
226+
): {
227+
path: Path;
228+
value: Writable<StringPathType<z.infer<UnwrapEffects<T>>, Path>>;
229+
errors: Writable<string[] | undefined>;
230+
constraints: Writable<InputConstraint | undefined>;
231+
} {
223232
const path2 = splitPath<z.infer<T>>(path);
224233
// Filter out array indices, the constraints structure doesn't contain these.
225234
const constraintsPath = (path2 as unknown[])

src/lib/schemaEntity.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { InputConstraints, InputConstraint } from './index.js';
1+
import {
2+
type InputConstraints,
3+
type InputConstraint,
4+
type ZodValidation,
5+
type UnwrapEffects,
6+
SuperFormError
7+
} from './index.js';
28

39
import type { ZodTypeInfo } from './traversal.js';
410

@@ -9,7 +15,7 @@ import {
915
type ZodDefault,
1016
type ZodNullable,
1117
type ZodOptional,
12-
type ZodEffects,
18+
ZodEffects,
1319
ZodString,
1420
ZodNumber,
1521
ZodBoolean,
@@ -235,26 +241,32 @@ export function valueOrDefault(
235241
* Returns the default values for a zod validation schema.
236242
* The main gotcha is that undefined values are changed to null if the field is nullable.
237243
*/
238-
export function defaultValues<T extends AnyZodObject>(
244+
export function defaultValues<T extends ZodValidation<AnyZodObject>>(
239245
schema: T
240-
): z.infer<T> {
241-
const fields = Object.keys(schema.keyof().Values);
246+
): z.infer<UnwrapEffects<T>> {
247+
while (schema instanceof ZodEffects) {
248+
schema = (schema as ZodEffects<T>)._def.schema;
249+
}
242250

243-
let output: Record<string, unknown> = {};
251+
if (!(schema instanceof ZodObject)) {
252+
throw new SuperFormError(
253+
'Only Zod schema objects can be used with defaultValues. ' +
254+
'Define the schema with z.object({ ... }) and optionally refine/superRefine/transform at the end.'
255+
);
256+
}
244257

245-
const schemaTypeInfo = schemaInfo(schema);
258+
const realSchema = schema as UnwrapEffects<T>;
259+
const fields = Object.keys(realSchema.keyof().Values);
260+
const schemaTypeInfo = schemaInfo(realSchema);
246261

247-
// Need to set empty properties after defaults are set.
248-
output = Object.fromEntries(
262+
return Object.fromEntries(
249263
fields.map((field) => {
250264
const typeInfo = schemaTypeInfo[field];
251265
const newValue = valueOrDefault(undefined, true, true, typeInfo);
252266

253267
return [field, newValue];
254268
})
255269
);
256-
257-
return output;
258270
}
259271

260272
function constraints<T extends AnyZodObject>(

src/lib/stringPath.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
11
import type { FieldPath } from './index.js';
22

3-
export function splitPath<T extends object>(path: StringPath<T>) {
3+
/*
4+
type Expand<T> = T extends (...args: infer A) => infer R
5+
? (...args: Expand<A>) => Expand<R>
6+
: T extends infer O
7+
? { [K in keyof O]: O[K] }
8+
: never;
9+
10+
type ExpandRecursively<T> = T extends (...args: infer A) => infer R
11+
? (...args: ExpandRecursively<A>) => ExpandRecursively<R>
12+
: T extends object
13+
? T extends infer O
14+
? { [K in keyof O]: ExpandRecursively<O[K]> }
15+
: never
16+
: T;
17+
18+
type FilterObjects<T extends object> = {
19+
[K in keyof T]: T[K] extends object ? never : K;
20+
}[keyof T];
21+
*/
22+
23+
export function splitPath<T extends object>(
24+
path: StringPath<T> | StringPathLeaves<T>
25+
) {
426
return path
527
.toString()
628
.split(/[[\].]+/)
729
.filter((p) => p) as FieldPath<T>;
830
}
931

32+
/**
33+
* Lists all paths in an object as string accessors.
34+
*/
1035
export type StringPath<T extends object> = NonNullable<T> extends (infer U)[]
1136
? NonNullable<U> extends object
1237
?
@@ -33,6 +58,38 @@ export type StringPath<T extends object> = NonNullable<T> extends (infer U)[]
3358
}[keyof T]
3459
: never;
3560

61+
/**
62+
* Like StringPath, but only with non-objects as accessible properties.
63+
* As the leaves in a node tree, if you look at the object as a tree structure.
64+
*/
65+
export type StringPathLeaves<T extends object> =
66+
NonNullable<T> extends (infer U)[]
67+
? NonNullable<U> extends object
68+
? `[${number}]${U extends unknown[]
69+
? ''
70+
: '.'}${NonNullable<U> extends Date
71+
? never
72+
: StringPathLeaves<NonNullable<U>> & string}`
73+
: `[${number}]`
74+
: NonNullable<T> extends object
75+
?
76+
| {
77+
// Same as FilterObjects but inlined for better intellisense
78+
[K in keyof T]: T[K] extends object ? never : K;
79+
}[keyof T]
80+
| {
81+
[K in keyof T]-?: K extends string
82+
? NonNullable<T[K]> extends object
83+
? `${K}${NonNullable<T[K]> extends unknown[]
84+
? ''
85+
: '.'}${NonNullable<T[K]> extends Date
86+
? never
87+
: StringPathLeaves<NonNullable<T[K]>> & string}`
88+
: never
89+
: never;
90+
}[keyof T]
91+
: never;
92+
3693
export type StringPathType<T, P extends string> = P extends keyof T
3794
? T[P]
3895
: P extends number

src/lib/superValidate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
type SafeParseReturnType
3636
} from 'zod';
3737

38-
import { splitPath, type StringPath } from './stringPath.js';
38+
import { splitPath, type StringPathLeaves } from './stringPath.js';
3939

4040
import { clone } from './utils.js';
4141

@@ -63,7 +63,7 @@ export const setMessage = message;
6363
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6464
export function setError<T extends ZodValidation<AnyZodObject>>(
6565
form: Validation<T, unknown>,
66-
path: StringPath<z.infer<UnwrapEffects<T>>>,
66+
path: StringPathLeaves<z.infer<UnwrapEffects<T>>>,
6767
error: string | string[],
6868
options: { overwrite?: boolean; status?: number } = {
6969
overwrite: false,

src/lib/traversal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function mapErrors<T extends AnyZodObject>(
4646
return output as ValidationErrors<T>;
4747
}
4848

49+
// TODO: Can path be a StringPath?
4950
export function findErrors(
5051
errors: ValidationErrors<AnyZodObject>,
5152
path: string[] = []

src/paths.test-d.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
2-
import type { StringPath, StringPathType } from '$lib/stringPath';
2+
import type {
3+
StringPath,
4+
StringPathType,
5+
StringPathLeaves
6+
} from '$lib/stringPath';
37
import { test } from 'vitest';
48

59
type Obj = {
@@ -70,3 +74,19 @@ test('StringPathType', () => {
7074
// @ts-expect-error incorrect path
7175
const n2: StringPathType<Obj, 'nope incorrect'> = 'never';
7276
});
77+
78+
test('StringPathLeaves', () => {
79+
const o = {
80+
test: [1, 2, 3],
81+
name: 'name',
82+
other: [{ test: 'a', ok: 123 }, { test: 'b' }],
83+
obj: {
84+
ok: 123,
85+
arr: [1, 2, 3],
86+
test: '1231231',
87+
next: [{ level: 1 }, { level: 2 }]
88+
}
89+
};
90+
91+
const p: StringPathLeaves<typeof o> = 'test[3]';
92+
});

0 commit comments

Comments
 (0)