zodselect 1.0.15 → 1.0.17

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.
@@ -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>;
@@ -33,7 +33,7 @@ export type RefineZodUnion<T extends ZodUnion> = {
33
33
  [K in keyof UnionMergedShape<T>]?: UnionMergedShape<T>[K] extends ZodType ? UnionFieldType<UnionMergedShape<T>[K]> : boolean;
34
34
  };
35
35
  export type TuplifyUnion<T, TDepth extends number = 0, L = LastOf<T>, N = [T] extends [never] ? true : false> = TDepth extends 9 ? [] : true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>, NextDepth<TDepth>>, InternalZodify<L>>;
36
- type UnionFieldType<T extends ZodType> = z.infer<T> 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;
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
39
  export type Zodify<T, TDepth extends number = 0> = ApplyOptionalNullable<T, IsTuple<TuplifyUnion<Exclude<T, null | undefined>, TDepth>, ZodUnion<TuplifyUnion<Exclude<T, null | undefined>, TDepth>>, InternalZodify<Exclude<T, null | undefined>, TDepth>>>;
@@ -233,4 +233,78 @@ describe('.refineSchema()', () => {
233
233
  parseResult = schema.parse('123 Main Street');
234
234
  deepStrictEqual(parseResult, '123 Main Street');
235
235
  });
236
+ it('should accept selecting a subfield of a union member with an optional array-of-objects field', () => {
237
+ // Regression test for a bug in `UnionFieldType` (refine-schema.ts).
238
+ //
239
+ // `UnionFieldType` chose a union member field's refine shape with
240
+ // `z.infer<T> extends Array<infer U>` but, unlike `RefineType`, it did not
241
+ // strip `| undefined` first. For an OPTIONAL array-of-objects member
242
+ // (`field: Obj.array().optional()`), `z.infer<T>` is `Obj[] | undefined`,
243
+ // which is NOT assignable to `Array<infer U>`, so the field's refine shape
244
+ // collapsed to `RefineObject<Obj[]>` (the array's own keys) instead of
245
+ // `RefineObject<Obj>`. The runtime was unaffected, so this only broke the
246
+ // type-check (`tsc`), not `tsx --test`.
247
+ const accessLevel = z.object({
248
+ _id: z.string(),
249
+ name: z.string()
250
+ });
251
+ const organizationInvitation = z.object({
252
+ accessLevels: accessLevel.array().optional(),
253
+ organizationId: z.string(),
254
+ type: z.literal('organization')
255
+ });
256
+ const platformInvitation = z.object({
257
+ accessLevels: accessLevel.array().optional(),
258
+ type: z.literal('platform')
259
+ });
260
+ const invitation = z.discriminatedUnion('type', [organizationInvitation, platformInvitation]);
261
+ // Type-level assertion: drilling into a subfield of the optional array
262
+ // member is a valid select and MUST be assignable to the refine shape.
263
+ // With the bug present, this `satisfies` fails to compile.
264
+ const select = {
265
+ accessLevels: { name: true },
266
+ type: true
267
+ };
268
+ const refined = refineSchema(invitation, select);
269
+ const parsed = refined.parse({
270
+ accessLevels: [{ name: 'Admin' }],
271
+ type: 'organization'
272
+ });
273
+ assert.equal(parsed.type, 'organization');
274
+ assert.deepEqual(parsed.accessLevels, [{ name: 'Admin' }]);
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
+ });
236
310
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, it } from 'node:test';
2
+ import { z } from 'zod';
3
+ const internalService = z.object({
4
+ name: z.string(),
5
+ type: z.literal('internal')
6
+ }).strict();
7
+ const externalService = z.object({
8
+ iconUrl: z.string().optional(),
9
+ name: z.string(),
10
+ type: z.literal('external')
11
+ }).strict();
12
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
13
+ const serviceType = z.union([internalService, externalService]);
14
+ describe('InferMergedType', () => {
15
+ it('should resolve to z.infer<T> when used in a generic function with TSelect = undefined', () => {
16
+ function getServices(options) {
17
+ void options;
18
+ return {};
19
+ }
20
+ // When called without select, TSelect defaults to undefined
21
+ // InferMergedType<typeof Service, undefined> should resolve to z.infer<typeof Service>
22
+ const services = getServices({});
23
+ const service = Object.values(services)[0];
24
+ // This is a compile-time test: the assertion below must type-check (all
25
+ // union members have `name: string`). The impl returns `{}`, so guard the
26
+ // dereference — the block never runs but TS still checks it.
27
+ if (service) {
28
+ const name = service.name;
29
+ void name;
30
+ }
31
+ });
32
+ it('should resolve to projected type when used in a generic function with a select', () => {
33
+ function getServices(options) {
34
+ void options;
35
+ return {};
36
+ }
37
+ // When called with a select, the return type should be the projected type
38
+ const services = getServices({ select: { name: true } });
39
+ const service = Object.values(services)[0];
40
+ // This is a compile-time test: the assertion below must type-check (name
41
+ // was selected). The impl returns `{}`, so guard the dereference — the
42
+ // block never runs but TS still checks it.
43
+ if (service) {
44
+ const name = service.name;
45
+ void name;
46
+ }
47
+ });
48
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zodselect",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
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",