zodselect 1.0.16 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/refine-schema.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type RefinedUnionSchema<T extends ZodUnion, TShape> = ZodUnion<T['options
|
|
|
22
22
|
] : never : never>;
|
|
23
23
|
type Refinement<T extends ZodType, TIsSimple extends boolean> = TIsSimple extends true ? boolean : ((schema: T) => ZodType) | boolean | ZodType;
|
|
24
24
|
export type RefineObject<T extends object, TIsSimple extends boolean> = {
|
|
25
|
-
[K in keyof T]?: RefineType<T[K], TIsSimple>;
|
|
25
|
+
[K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]?: RefineType<T[K], TIsSimple>;
|
|
26
26
|
};
|
|
27
27
|
export type RefineSchema<T extends ZodType<object> | ZodUnion, TIsSimple extends boolean> = T extends ZodUnion ? RefineZodUnion<T> : T extends ZodType<object> ? RefineObject<z.infer<T>, TIsSimple> : never;
|
|
28
28
|
type RefineType<T, TIsSimple extends boolean> = IsTuple<TuplifyUnion<Exclude<T, null | undefined>>, Refinement<Zodify<T>, TIsSimple> | RefineZodUnion<Extract<T, ZodUnion>>, Exclude<T, null | undefined> extends string ? Refinement<ApplyOptionalNullable<T, ZodString>, TIsSimple> : Exclude<T, null | undefined> extends number ? Refinement<ApplyOptionalNullable<T, ZodNumber>, TIsSimple> : Exclude<T, null | undefined> extends boolean ? Refinement<ApplyOptionalNullable<T, ZodBoolean>, TIsSimple> : Exclude<T, null | undefined> extends Date ? Refinement<ApplyOptionalNullable<T, ZodDate>, TIsSimple> : Exclude<T, null | undefined> extends Array<infer U> ? Exclude<U, null | undefined> extends object ? Refinement<Zodify<T>, TIsSimple> | RefineObject<Extract<U, object>, TIsSimple> : Refinement<Zodify<T>, TIsSimple> : Exclude<T, null | undefined> extends object ? IsJsonType<Exclude<T, null | undefined>, false, Refinement<Zodify<T>, TIsSimple> | RefineObject<Extract<T, object>, TIsSimple>, Refinement<Zodify<T>, TIsSimple>> : unknown extends Exclude<T, null | undefined> ? Refinement<Zodify<T>, TIsSimple> : never>;
|
|
@@ -36,6 +36,8 @@ export type TuplifyUnion<T, TDepth extends number = 0, L = LastOf<T>, N = [T] ex
|
|
|
36
36
|
type UnionFieldType<T extends ZodType> = Exclude<z.infer<T>, null | undefined> extends Array<infer U> ? Exclude<U, null | undefined> extends object ? boolean | RefineObject<Extract<U, object>, true> : boolean : Exclude<z.infer<T>, null | undefined> extends object ? boolean | RefineObject<Exclude<z.infer<T>, null | undefined>, true> : boolean;
|
|
37
37
|
type UnionMergedShape<T extends ZodUnion> = UnionToIntersection<T['options'][number] extends ZodObject<infer Shape extends ZodRawShape> ? Shape : object>;
|
|
38
38
|
export type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
39
|
-
export type Zodify<T, TDepth extends number = 0> = ApplyOptionalNullable<T,
|
|
39
|
+
export type Zodify<T, TDepth extends number = 0> = ApplyOptionalNullable<T, IsAny<T, ZodAny, [
|
|
40
|
+
Exclude<T, null | undefined>
|
|
41
|
+
] extends [string] ? string extends Exclude<T, null | undefined> ? ZodString : ZodLiteral<Exclude<T, null | undefined>> : IsTuple<TuplifyUnion<Exclude<T, null | undefined>, TDepth>, ZodUnion<TuplifyUnion<Exclude<T, null | undefined>, TDepth>>, InternalZodify<Exclude<T, null | undefined>, TDepth>>>>;
|
|
40
42
|
export declare function refineSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T, false>>(schema: T, shape: TShape): RefinedSchema<T, TShape, false>;
|
|
41
43
|
export {};
|
|
@@ -273,4 +273,86 @@ describe('.refineSchema()', () => {
|
|
|
273
273
|
assert.equal(parsed.type, 'organization');
|
|
274
274
|
assert.deepEqual(parsed.accessLevels, [{ name: 'Admin' }]);
|
|
275
275
|
});
|
|
276
|
+
it('allows selecting a nested subfield of a declared looseObject field', () => {
|
|
277
|
+
// Regression test for `RefineObject` collapsing a `z.looseObject`'s declared fields.
|
|
278
|
+
//
|
|
279
|
+
// A `z.looseObject` carries a catchall index signature (`[k: string]: unknown`).
|
|
280
|
+
// `RefineObject` maps over `keyof T`, and when `T` has an index signature `keyof T`
|
|
281
|
+
// collapses to `string | number`, so every declared field's value type came from the
|
|
282
|
+
// catchall (`unknown` -> scalar `boolean`). Declared fields could be marked `true` but
|
|
283
|
+
// could NOT be drilled into. The runtime was unaffected, so this only broke the
|
|
284
|
+
// type-check (`tsc`), not `tsx --test`.
|
|
285
|
+
const personName = z.object({
|
|
286
|
+
first: z.string().optional(),
|
|
287
|
+
last: z.string().optional()
|
|
288
|
+
});
|
|
289
|
+
const user = z.looseObject({
|
|
290
|
+
_id: z.string(),
|
|
291
|
+
name: personName.optional(),
|
|
292
|
+
username: z.string()
|
|
293
|
+
});
|
|
294
|
+
const root = z.object({
|
|
295
|
+
invitedBy: user
|
|
296
|
+
});
|
|
297
|
+
// Type-level assertion: drilling into `invitedBy.name.first` is valid and MUST be
|
|
298
|
+
// assignable to the refine shape. With the bug present this `satisfies` fails to
|
|
299
|
+
// compile because `invitedBy`'s declared fields collapse to an index signature with
|
|
300
|
+
// `boolean` values.
|
|
301
|
+
const select = {
|
|
302
|
+
invitedBy: { name: { first: true } }
|
|
303
|
+
};
|
|
304
|
+
const refined = refineSchema(root, select);
|
|
305
|
+
const parsed = refined.parse({
|
|
306
|
+
invitedBy: { name: { first: 'Alice' } }
|
|
307
|
+
});
|
|
308
|
+
assert.equal(parsed.invitedBy.name?.first, 'Alice');
|
|
309
|
+
});
|
|
310
|
+
it('keeps every option of a small enum in the refined type (control)', () => {
|
|
311
|
+
const stage = z.enum(['one', 'two', 'three']);
|
|
312
|
+
const project = z.object({ name: z.string(), stage });
|
|
313
|
+
const refined = refineSchema(project, { stage: true });
|
|
314
|
+
// Type-level assertion: every option must remain assignable to the refined type.
|
|
315
|
+
const selected = [
|
|
316
|
+
{ stage: 'one' },
|
|
317
|
+
{ stage: 'two' },
|
|
318
|
+
{ stage: 'three' }
|
|
319
|
+
];
|
|
320
|
+
deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
|
|
321
|
+
});
|
|
322
|
+
it('keeps every option of an enum wider than nine options in the refined type', () => {
|
|
323
|
+
// Regression test for `TuplifyUnion` (refine-schema.ts) truncating wide enums.
|
|
324
|
+
//
|
|
325
|
+
// `TuplifyUnion` spends one recursion level per union member and stops at its
|
|
326
|
+
// depth cap, so an enum field refined with `true` silently dropped every option
|
|
327
|
+
// past nine from the refined TYPE — and which options vanished varied with
|
|
328
|
+
// TypeScript's internal union ordering. Fixed by the ZodLiteral shortcut in
|
|
329
|
+
// `Zodify`. The runtime was unaffected, so this only broke the type-check
|
|
330
|
+
// (`tsc`), not `tsx --test`.
|
|
331
|
+
const stage = z.enum([
|
|
332
|
+
'draft', 'interviewing', 'interview-complete',
|
|
333
|
+
'previewing', 'preview-review',
|
|
334
|
+
'plan-review', 'plan-approved', 'local',
|
|
335
|
+
'spec-review', 'spec-approved',
|
|
336
|
+
'implementing', 'module-ready'
|
|
337
|
+
]);
|
|
338
|
+
const project = z.object({ name: z.string(), stage });
|
|
339
|
+
const refined = refineSchema(project, { stage: true });
|
|
340
|
+
// Type-level assertion: each of the twelve options must remain assignable to
|
|
341
|
+
// the refined type. With the bug present, three of these assignments fail TS2322.
|
|
342
|
+
const selected = [
|
|
343
|
+
{ stage: 'draft' },
|
|
344
|
+
{ stage: 'interviewing' },
|
|
345
|
+
{ stage: 'interview-complete' },
|
|
346
|
+
{ stage: 'previewing' },
|
|
347
|
+
{ stage: 'preview-review' },
|
|
348
|
+
{ stage: 'plan-review' },
|
|
349
|
+
{ stage: 'plan-approved' },
|
|
350
|
+
{ stage: 'local' },
|
|
351
|
+
{ stage: 'spec-review' },
|
|
352
|
+
{ stage: 'spec-approved' },
|
|
353
|
+
{ stage: 'implementing' },
|
|
354
|
+
{ stage: 'module-ready' }
|
|
355
|
+
];
|
|
356
|
+
deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
|
|
357
|
+
});
|
|
276
358
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodselect",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Type-safe selection utility built on top of Zod. Allows for strongly typed field selection, schema merging, and runtime refinement for frontend or backend use.",
|
|
5
5
|
"author": "SecureDataSystems",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@secure-data-systems/eslint-config": "^1.0.
|
|
29
|
-
"@secure-data-systems/tsconfig": "^1.0.
|
|
30
|
-
"@types/node": "^
|
|
31
|
-
"tsx": "^4.
|
|
32
|
-
"typescript": "
|
|
28
|
+
"@secure-data-systems/eslint-config": "^1.0.38",
|
|
29
|
+
"@secure-data-systems/tsconfig": "^1.0.6",
|
|
30
|
+
"@types/node": "^26.1.1",
|
|
31
|
+
"tsx": "^4.23.1",
|
|
32
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
33
|
+
"typescript-native": "npm:typescript@^7.0.2"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"zod": "^4.3
|
|
36
|
+
"zod": "^4.4.3"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "node scripts/build.mjs && tsc --project tsconfig.build.json",
|