zodselect 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SecureDataSystems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # ZodSelect
2
+
3
+ **ZodSelect** is a strongly typed selection utility library built on top of [Zod](https://github.com/colinhacks/zod) (v4). It allows developers to create dynamic select statements with precise TypeScript inference — perfect for shaping responses in database queries, or APIs. `ZodSelect` ensures that both your types and runtime logic stay in sync — with no external dependencies.
4
+
5
+ - ✅ Built on Zod v4
6
+ - ✅ No external dependencies (except `zod`)
7
+ - ✅ Fully type-safe field selections
8
+ - ✅ Supports merging multiple Zod Selects
9
+ - ✅ Generate Zod schemas from Zod Selects
10
+ - ✅ Allows schema refinement and overrides
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add zod @securedatasystems/zod-select
18
+ # or
19
+ npm install zod @securedatasystems/zod-select
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Example Schema
25
+
26
+ All examples use the following Zod schema:
27
+
28
+ ```ts
29
+ const UserSchema = z.object({
30
+ email: z.string(),
31
+ firstName: z.string(),
32
+ lastName: z.string(),
33
+ address: z.object({
34
+ street: z.string(),
35
+ city: z.string(),
36
+ state: z.string(),
37
+ zip: z.string()
38
+ })
39
+ });
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 🧠 Zod Select with Inferred Type
45
+
46
+ A single selection object returns a strongly typed result:
47
+
48
+ ```ts
49
+ function find<T extends ZodSelect<typeof UserSchema>>(select: T): InferType<typeof UserSchema, T> {
50
+ // ...Find Logic
51
+ }
52
+
53
+ const user = find({
54
+ firstName: true,
55
+ lastName: true,
56
+ address: { street: true }
57
+ });
58
+ ```
59
+
60
+ ```ts
61
+ // user inferred as:
62
+ {
63
+ firstName: string;
64
+ lastName: string;
65
+ address: { street: string };
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 🔀 Merging Multiple Zod Selects
72
+
73
+ You can combine multiple select statements and still get precise inference:
74
+
75
+ ```ts
76
+ function find<T extends MultiSelect<typeof UserSchema>>(select: T): InferMergedType<typeof UserSchema, T> {
77
+ // ...Find Logic
78
+ }
79
+
80
+ const user = find([
81
+ { lastName: true },
82
+ { firstName: true }
83
+ ]);
84
+ ```
85
+
86
+ ```ts
87
+ // user inferred as:
88
+ {
89
+ firstName: string;
90
+ lastName: string;
91
+ }
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 🧹 Mix Zod Schemas with Zod Selects
97
+
98
+ You can mix plain field selections with Zod object schemas for flexible results:
99
+
100
+ ```ts
101
+ function find<T extends MultiSelect<typeof UserSchema>>(select: T): InferMergedType<typeof UserSchema, T> {
102
+ // ...Find Logic
103
+ }
104
+
105
+ const user = find([
106
+ { lastName: true },
107
+ z.object({
108
+ address: z.object({ state: z.string() })
109
+ })
110
+ ]);
111
+ ```
112
+
113
+ ```ts
114
+ // user inferred as:
115
+ {
116
+ lastName: string;
117
+ address: { state: string };
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 🛠️ Generate Zod Schemas from Zod Selects
124
+
125
+ Use `refineSchema` to turn a selection into a Zod schema:
126
+
127
+ ```ts
128
+ const schema = refineSchema(UserSchema, {
129
+ firstName: true,
130
+ address: true
131
+ });
132
+ ```
133
+
134
+ ```ts
135
+ // schema.shape:
136
+ {
137
+ firstName: z.ZodString;
138
+ address: z.ZodObject<...>;
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 🧪 Refine Field Types Inline
145
+
146
+ Selections can refine field types inline using Zod methods:
147
+
148
+ ```ts
149
+ const schema = refineSchema(UserSchema, {
150
+ firstName: s => s.optional()
151
+ });
152
+ ```
153
+
154
+ ```ts
155
+ // schema.shape:
156
+ {
157
+ firstName: z.ZodOptional<z.ZodString>;
158
+ }
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 🧬 Redefine Field Types
164
+
165
+ Selections can also redefine types entirely:
166
+
167
+ ```ts
168
+ const schema = refineSchema(UserSchema, {
169
+ firstName: z.number()
170
+ });
171
+ ```
172
+
173
+ ```ts
174
+ // schema.shape:
175
+ {
176
+ firstName: z.ZodNumber;
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## 🧵 Putting it all together
183
+
184
+ Combining all methods and types together you can build powerful, type-safe selection and inference using the zod schemas you already have.
185
+
186
+ ```ts
187
+ function find<T extends MultiSelect<typeof UserSchema>>(select: T): InferMergedType<typeof UserSchema, T>[] {
188
+ const selectArr = Array.isArray(select) ? select : [select];
189
+ const sel: Record<string, 1> = {};
190
+
191
+ for (let schema of selectArr) {
192
+ if (!(schema instanceof ZodType)) {
193
+ schema = refineSchema(UserSchema, schema);
194
+ continue;
195
+ }
196
+
197
+ // Crawl each zod schema building the select statement
198
+ }
199
+
200
+ // Find Logic
201
+ }
202
+
203
+ function getUsersFromTexas<T extends MultiSelect<typeof UserSchema>>(
204
+ sel: T
205
+ ): InferMergedType<typeof UserSchema, MergeSelect<typeof UserSchema, { address: boolean }, T>>[] {
206
+ const users = find(
207
+ // Use the merge select function to combined a ZodSelect with a MultiSelect
208
+ mergeSelect(UserSchema, { address: true }, sel)
209
+ );
210
+
211
+ return users.filter((user) =>
212
+ // At this point the user object has address and whatever fields are specified in the
213
+ // passed in select but VS code doesn't know what fields are going to be passed in at
214
+ // runtime. It does know that address was selected inside this function so it is
215
+ // strongly typed with a fully selected address object
216
+ user.address.state === 'TX'
217
+ );
218
+ }
219
+
220
+ const users = getUsersFromTexas({
221
+ firstName: true,
222
+ lastName: true
223
+ });
224
+
225
+ // At this point we know what fields we selected and the extra fields added by the
226
+ // getUsersFromTexas function
227
+ ```
228
+
229
+ ```ts
230
+ // user inferred as:
231
+ {
232
+ firstName: string;
233
+ lastName: string;
234
+ address: {
235
+ street: string;
236
+ city: string;
237
+ state: string;
238
+ zip: string;
239
+ };
240
+ }[]
241
+ ```
@@ -0,0 +1,3 @@
1
+ export * from './merge-select.js';
2
+ export * from './refine-schema.js';
3
+ export * from './select.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './merge-select.js';
2
+ export * from './refine-schema.js';
3
+ export * from './select.js';
@@ -0,0 +1,3 @@
1
+ import { ZodType, ZodUnion } from 'zod/v4';
2
+ import { MergeSelect, MultiSelect, ZodSelect } from './select.js';
3
+ export declare function mergeSelect<T extends ZodType<object> | ZodUnion, TSelect1 extends ZodSelect<T>, TSelect2 extends MultiSelect<T>>(schema: T, select1: TSelect1, select2?: TSelect2): MergeSelect<T, TSelect1, TSelect2>;
@@ -0,0 +1,9 @@
1
+ export function mergeSelect(schema, select1, select2) {
2
+ if (select2 === undefined) {
3
+ return [select1, schema];
4
+ }
5
+ if (Array.isArray(select2)) {
6
+ return [select1, ...select2];
7
+ }
8
+ return [select1, select2];
9
+ }
@@ -0,0 +1,36 @@
1
+ import { core, z, ZodAny, ZodArray, ZodBoolean, ZodDate, ZodLiteral, ZodNever, ZodNullable, ZodNumber, ZodObject, ZodOptional, ZodRawShape, ZodString, ZodType, ZodUnion, ZodUnknown } from 'zod/v4';
2
+ import { IsAny, IsJsonType } from './types.js';
3
+ export type RefinedSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T>> = T extends ZodUnion ? RefinedUnionSchema<T, TShape> : T extends ZodType<object> ? RefinedTypeSchema<z.infer<T>, TShape> : never;
4
+ export type RefinedTypeSchema<T extends object, TShape, TDepth extends number = 0> = TDepth extends keyof DepthLimit ? ZodObject<{
5
+ [K in keyof T & keyof TShape]: TShape[K] extends (schema: infer S) => ZodType ? ReturnType<TShape[K]> : TShape[K] extends ZodType ? TShape[K] : TShape[K] extends boolean ? Zodify<T[K]> : TShape[K] extends object ? Exclude<T[K], null | undefined> extends Array<infer U> ? U extends object ? ApplyOptionalNullable<T[K], z.ZodArray<RefinedTypeSchema<U, TShape[K], NextDepth<TDepth>>>> : Zodify<T[K]> : Exclude<T[K], null | undefined> extends object ? ApplyOptionalNullable<T[K], RefinedTypeSchema<Exclude<T[K], null | undefined>, TShape[K], NextDepth<TDepth>>> : ZodNever : ZodNever;
6
+ }, core.$strict> : any;
7
+ export type RefinedUnionSchema<T extends ZodUnion, TShape> = ZodUnion<T['options'] extends Readonly<[infer A, ...infer Rest]> ? Rest extends ZodType[] ? [
8
+ ReapplyOptionalNullable<A, A extends ZodType<object> ? RefinedTypeSchema<z.infer<A>, TShape> : Extract<A, ZodType>>,
9
+ ...{
10
+ [K in keyof Rest]: Rest[K] extends ZodType ? z.infer<Rest[K]> extends object ? RefinedTypeSchema<z.infer<Rest[K]>, TShape> : Extract<Rest[K], ZodType> : Extract<Rest[K], ZodType>;
11
+ }
12
+ ] : never : never>;
13
+ export type RefineObject<T extends object> = {
14
+ [K in keyof T]?: RefineType<T[K]>;
15
+ };
16
+ export type RefineSchema<T extends ZodType<object> | ZodUnion> = T extends ZodUnion ? RefineZodUnion<T> : T extends ZodType<object> ? RefineObject<z.infer<T>> : never;
17
+ export type RefineZodUnion<T extends ZodUnion> = {
18
+ [K in keyof UnionToIntersection<T['options'][number] extends ZodObject<infer Shape extends ZodRawShape> ? Shape : object>]?: boolean;
19
+ };
20
+ 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>>;
21
+ export type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
22
+ 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>>>;
23
+ type ApplyOptionalNullable<T, U extends ZodType> = IsAny<T, U, undefined extends T ? null extends T ? ZodOptional<ZodNullable<U>> : ZodOptional<U> : null extends T ? ZodNullable<U> : U>;
24
+ type DepthLimit = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
25
+ type InternalZodify<T, TDepth extends number = 0> = TDepth extends 9 ? ZodType : IsAny<T, ZodAny, T extends string ? string extends T ? ZodString : ZodLiteral<T> : T extends number ? ZodNumber : T extends boolean ? ZodBoolean : T extends Date ? ZodDate : unknown extends T ? ZodUnknown : T extends Array<infer U> ? ZodArray<Zodify<U, NextDepth<TDepth>>> : T extends object ? IsJsonType<T, true, ZodObject<{
26
+ [K in keyof T]-?: Zodify<T[K], NextDepth<TDepth>>;
27
+ }, core.$strict>, ZodType<T>> : never>;
28
+ type IsTuple<T, TTrue = true, TFalse = false> = T extends [infer TFirst, infer TSecond, ...infer TRest] ? TTrue : TFalse;
29
+ type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never;
30
+ type NextDepth<T extends number> = T extends 0 ? 1 : T extends 1 ? 2 : T extends 2 ? 3 : T extends 3 ? 4 : T extends 4 ? 5 : T extends 5 ? 6 : T extends 6 ? 7 : T extends 7 ? 8 : T extends 8 ? 9 : 9;
31
+ type Push<T extends any[], V> = [...T, V];
32
+ type ReapplyOptionalNullable<T, U extends z.ZodType> = T extends ZodOptional<ZodNullable<ZodType>> ? ZodOptional<ZodNullable<U>> : T extends ZodNullable<ZodOptional<ZodType>> ? ZodNullable<ZodOptional<U>> : T extends ZodOptional<ZodType> ? ZodOptional<U> : T extends ZodNullable<ZodType> ? ZodNullable<U> : U;
33
+ type Refinement<T extends ZodType> = ((schema: T) => ZodType) | boolean | ZodType;
34
+ type RefineType<T> = IsTuple<TuplifyUnion<Exclude<T, null | undefined>>, Refinement<Zodify<T>>, Exclude<T, null | undefined> extends string ? Refinement<ApplyOptionalNullable<T, ZodString>> : Exclude<T, null | undefined> extends number ? Refinement<ApplyOptionalNullable<T, ZodNumber>> : Exclude<T, null | undefined> extends boolean ? Refinement<ApplyOptionalNullable<T, ZodBoolean>> : Exclude<T, null | undefined> extends Date ? Refinement<ApplyOptionalNullable<T, ZodDate>> : Exclude<T, null | undefined> extends Array<infer U> ? Exclude<U, null | undefined> extends object ? Refinement<Zodify<T>> | RefineObject<Extract<U, object>> : Refinement<Zodify<T>> : Exclude<T, null | undefined> extends object ? IsJsonType<Exclude<T, null | undefined>, false, Refinement<Zodify<T>> | RefineObject<Extract<T, object>>, Refinement<Zodify<T>>> : unknown extends Exclude<T, null | undefined> ? Refinement<Zodify<T>> : never>;
35
+ export declare function refineSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T>>(schema: T, shape: TShape): RefinedSchema<T, TShape>;
36
+ export {};
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod/v4';
2
+ import { getInnerType, isZodArray, isZodNullable, isZodObject, isZodOptional, isZodType, isZodUnion } from './utilities.js';
3
+ // Main function to refine the schema
4
+ export function refineSchema(schema, shape) {
5
+ if (isZodObject(schema)) {
6
+ const refinedShape = {};
7
+ const schemaShape = schema.shape;
8
+ for (const key in shape) {
9
+ if (key in schemaShape) {
10
+ refinedShape[key] = refineSchemaField(schemaShape[key], shape[key]);
11
+ }
12
+ }
13
+ return z.object(refinedShape);
14
+ }
15
+ else if (isZodUnion(schema)) {
16
+ const refinedOptions = schema.options.map(option => refineSchemaField(option, shape));
17
+ return z.union(refinedOptions);
18
+ }
19
+ throw new Error('Unsupported schema type for refinement');
20
+ }
21
+ // Helper function to refine a single schema field
22
+ function refineSchemaField(fieldSchema, fieldShape) {
23
+ if (fieldShape === true) {
24
+ return fieldSchema; // Keep original type
25
+ }
26
+ else if (typeof fieldShape === 'function') {
27
+ return fieldShape(fieldSchema); // Apply refinement function
28
+ }
29
+ else if (isZodType(fieldShape)) {
30
+ return fieldShape; // Use redefined type
31
+ }
32
+ else if (isZodObject(fieldSchema)) {
33
+ return refineSchema(fieldSchema, fieldShape); // Recursively refine nested objects
34
+ }
35
+ else if (isZodUnion(fieldSchema)) {
36
+ return refineSchema(fieldSchema, fieldShape);
37
+ }
38
+ else if (isZodArray(fieldSchema)) {
39
+ // Recursively refine array items
40
+ return z.array(refineSchemaField(fieldSchema.element, fieldShape));
41
+ }
42
+ else if (isZodOptional(fieldSchema)) {
43
+ return refineSchemaField(fieldSchema._def.innerType, fieldShape).optional();
44
+ }
45
+ else if (isZodNullable(fieldSchema)) {
46
+ return refineSchemaField(fieldSchema._def.innerType, fieldShape).nullable();
47
+ }
48
+ else {
49
+ const innerType = getInnerType(fieldSchema);
50
+ // There are wrapper types we can't forward because the underlying object structures are changing.
51
+ if (innerType) {
52
+ return refineSchemaField(innerType, fieldShape);
53
+ }
54
+ }
55
+ return fieldSchema;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,223 @@
1
+ import { strict as assert, deepStrictEqual, throws } from 'assert';
2
+ import { describe, it } from 'node:test';
3
+ import { z, ZodError } from 'zod/v4';
4
+ import { refineSchema } from './refine-schema.js';
5
+ describe('.refineSchema()', () => {
6
+ it('should retain required fields', () => {
7
+ const UserSchema = z.object({
8
+ age: z.number().optional(),
9
+ firstName: z.string(),
10
+ lastName: z.string()
11
+ });
12
+ const RefinedSchema = refineSchema(UserSchema, {
13
+ firstName: true,
14
+ lastName: true
15
+ });
16
+ const parsedData = RefinedSchema.parse({
17
+ firstName: 'John',
18
+ lastName: 'Doe'
19
+ });
20
+ assert.equal(parsedData.firstName, 'John');
21
+ assert.equal(parsedData.lastName, 'Doe');
22
+ });
23
+ it('should handle optional fields', () => {
24
+ const UserSchema = z.object({
25
+ age: z.number().optional(),
26
+ firstName: z.string(),
27
+ lastName: z.string()
28
+ });
29
+ const RefinedSchema = refineSchema(UserSchema, {
30
+ age: true
31
+ });
32
+ const parsedData = RefinedSchema.parse({ age: 25 });
33
+ assert.equal(parsedData.age, 25);
34
+ const parsedWithoutAge = RefinedSchema.parse({});
35
+ assert.equal(parsedWithoutAge.age, undefined);
36
+ });
37
+ it('should handle nullable fields', () => {
38
+ const RoleSchema = z.object({
39
+ name: z.string()
40
+ });
41
+ const UserSchema = z.object({
42
+ firstName: z.string(),
43
+ role: RoleSchema.nullable()
44
+ });
45
+ const RefinedSchema = refineSchema(UserSchema, {
46
+ role: true
47
+ });
48
+ const parsedWithNullRole = RefinedSchema.parse({
49
+ firstName: 'Alice',
50
+ role: null
51
+ });
52
+ assert.equal(parsedWithNullRole.role, null);
53
+ });
54
+ it('should handle both optional and nullable fields', () => {
55
+ const RoleSchema = z.object({
56
+ name: z.string()
57
+ });
58
+ const UserSchema = z.object({
59
+ firstName: z.string(),
60
+ role: RoleSchema.nullable().optional()
61
+ });
62
+ const RefinedSchema = refineSchema(UserSchema, {
63
+ role: {
64
+ name: true
65
+ }
66
+ });
67
+ const parsedWithRole = RefinedSchema.parse({
68
+ firstName: 'Alice',
69
+ role: { name: 'Admin' }
70
+ });
71
+ assert.deepStrictEqual(parsedWithRole.role, { name: 'Admin' });
72
+ const parsedWithNullRole = RefinedSchema.parse({
73
+ firstName: 'Alice',
74
+ role: null
75
+ });
76
+ assert.strictEqual(parsedWithNullRole.role, null);
77
+ const parsedWithoutRole = RefinedSchema.parse({ firstName: 'Alice' });
78
+ assert.strictEqual(parsedWithoutRole.role, undefined);
79
+ });
80
+ it('should apply custom schema transformations', () => {
81
+ const RoleSchema = z.object({
82
+ name: z.string()
83
+ });
84
+ const UserSchema = z.object({
85
+ firstName: z.string(),
86
+ role: RoleSchema
87
+ });
88
+ const RefinedSchema = refineSchema(UserSchema, {
89
+ role: (schema) => schema.extend({ permissions: z.array(z.string()) })
90
+ });
91
+ const parsedData = RefinedSchema.parse({
92
+ firstName: 'Bob',
93
+ role: { name: 'User', permissions: ['read', 'write'] }
94
+ });
95
+ assert.deepEqual(parsedData.role, {
96
+ name: 'User',
97
+ permissions: ['read', 'write']
98
+ });
99
+ });
100
+ it('should handle nested arrays', () => {
101
+ const RoleSchema = z.object({
102
+ level: z.number(),
103
+ title: z.string()
104
+ });
105
+ const UserSchema = z.object({
106
+ age: z.number().optional(),
107
+ firstName: z.string(),
108
+ lastName: z.string(),
109
+ nestedArray: z.array(z.object({ name: z.string(), value: z.number() })).optional().nullable(),
110
+ role: RoleSchema.nullable().optional(),
111
+ tags: z.array(z.string()).optional()
112
+ });
113
+ const RefinedSchema = refineSchema(UserSchema, {
114
+ nestedArray: {
115
+ name: true
116
+ }
117
+ });
118
+ const parsedData = RefinedSchema.parse({
119
+ firstName: 'Alice',
120
+ lastName: 'Smith',
121
+ nestedArray: [{ name: 'Feature1', value: 10 }, { name: 'Feature2', value: 20 }]
122
+ });
123
+ assert.deepStrictEqual(parsedData.nestedArray, [{ name: 'Feature1' }, { name: 'Feature2' }]);
124
+ // Test optional array
125
+ const parsedWithoutNestedArray = RefinedSchema.parse({
126
+ firstName: 'Alice',
127
+ lastName: 'Smith'
128
+ });
129
+ assert.equal(parsedWithoutNestedArray.nestedArray, undefined);
130
+ });
131
+ it('should handle custom fields', () => {
132
+ const UserSchema = z.object({
133
+ address: z.custom().optional(),
134
+ firstName: z.string(),
135
+ lastName: z.string()
136
+ });
137
+ const RefinedSchema = refineSchema(UserSchema, {
138
+ address: true
139
+ });
140
+ const parsedData = RefinedSchema.parse({
141
+ address: {
142
+ city: 'Austin',
143
+ state: 'TX',
144
+ street: '123 Main Street'
145
+ },
146
+ firstName: 'Alice',
147
+ lastName: 'Smith'
148
+ });
149
+ assert.deepStrictEqual(parsedData, {
150
+ address: {
151
+ city: 'Austin',
152
+ state: 'TX',
153
+ street: '123 Main Street'
154
+ }
155
+ });
156
+ });
157
+ it('should support unions of objects', () => {
158
+ const Person = z.object({
159
+ firstName: z.string(),
160
+ lastName: z.string()
161
+ });
162
+ const Address = z.object({
163
+ address: z.string(),
164
+ city: z.string()
165
+ });
166
+ const Schema = refineSchema(z.union([Person, Address]), {
167
+ address: true,
168
+ firstName: true
169
+ });
170
+ let parseResult = Schema.parse({
171
+ address: '123 Main Street',
172
+ firstName: 'Jerry',
173
+ lastName: 'Springer'
174
+ });
175
+ deepStrictEqual(parseResult, { firstName: 'Jerry' });
176
+ parseResult = Schema.parse({
177
+ address: '123 Main Street',
178
+ city: 'Austin'
179
+ });
180
+ deepStrictEqual(parseResult, { address: '123 Main Street' });
181
+ });
182
+ it('should error if none of the types are matched', () => {
183
+ const Person = z.object({
184
+ firstName: z.string(),
185
+ lastName: z.string()
186
+ });
187
+ const Address = z.object({
188
+ address: z.string(),
189
+ city: z.string()
190
+ });
191
+ const Schema = refineSchema(z.union([Person, Address]), {
192
+ address: true,
193
+ firstName: true
194
+ });
195
+ throws(() => {
196
+ Schema.parse({
197
+ lastName: 'Springer'
198
+ });
199
+ }, (err) => {
200
+ if (!(err instanceof ZodError) || err.issues[0].code !== 'invalid_union') {
201
+ return false;
202
+ }
203
+ return true;
204
+ });
205
+ });
206
+ it('should support unions of objects and simple types', () => {
207
+ const Person = z.object({
208
+ firstName: z.string(),
209
+ lastName: z.string()
210
+ });
211
+ const Schema = refineSchema(z.union([Person, z.string()]), {
212
+ address: true,
213
+ firstName: true
214
+ });
215
+ let parseResult = Schema.parse({
216
+ firstName: 'Jerry',
217
+ lastName: 'Springer'
218
+ });
219
+ deepStrictEqual(parseResult, { firstName: 'Jerry' });
220
+ parseResult = Schema.parse('123 Main Street');
221
+ deepStrictEqual(parseResult, '123 Main Street');
222
+ });
223
+ });
@@ -0,0 +1,12 @@
1
+ import { z, ZodType, ZodUnion } from 'zod/v4';
2
+ import { RefinedTypeSchema, RefinedUnionSchema, RefineObject, RefineZodUnion } from './refine-schema.js';
3
+ import { DeepMergeAll, IsEqual } from './types.js';
4
+ export type InferMergedType<T extends ZodType<object> | ZodUnion, U extends readonly ZodSelect<T>[] | undefined | ZodSelect<T>> = U extends undefined ? z.infer<T> : U extends readonly ZodSelect<T>[] ? DeepMergeAll<InferTuple<T, U>> : U extends ZodSelect<T> ? InferType<T, U> : never;
5
+ export type InferType<T extends ZodType<object> | ZodUnion, U extends ZodSelect<T>> = U extends ZodType<object> ? z.infer<U> : U extends ZodUnion ? z.infer<U> : T extends ZodUnion ? (U extends RefineZodUnion<T> ? z.infer<RefinedUnionSchema<T, U>> : z.infer<T>) : T extends ZodType<object> ? (U extends RefineObject<z.infer<T>> ? z.infer<RefinedTypeSchema<z.infer<T>, U>> : z.infer<T>) : z.infer<T>;
6
+ export type MergeSelect<T extends ZodType<object> | ZodUnion, TSelect1 extends ZodSelect<T>, TSelect2 extends MultiSelect<T>> = TSelect2 extends undefined ? [TSelect1, T] : IsEqual<TSelect2, T, TSelect1, TSelect2 extends [ZodSelect<T>, ...ZodSelect<T>[]] ? [TSelect1, ...TSelect2] : [TSelect1, TSelect2]>;
7
+ export type MultiSelect<T extends ZodType<object> | ZodUnion> = [ZodSelect<T>, ...ZodSelect<T>[]] | undefined | ZodSelect<T>;
8
+ export type ZodSelect<T extends ZodType<object> | ZodUnion> = (T extends ZodUnion ? (RefineZodUnion<T> | ZodUnion) : T extends ZodType<object> ? (RefineObject<z.infer<T>> | ZodType<object>) : never);
9
+ type InferTuple<T extends ZodType<object> | ZodUnion, U extends readonly ZodSelect<T>[]> = {
10
+ [K in keyof U]: InferType<T, U[K]>;
11
+ };
12
+ export {};
package/dist/select.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ export type DeepMerge<T, U> = {
2
+ [K in keyof T | keyof U]: K extends keyof T ? K extends keyof U ? T[K] extends object ? U[K] extends object ? DeepMerge<T[K], U[K]> : T[K] | U[K] : T[K] | U[K] : T[K] : K extends keyof U ? U[K] : never;
3
+ };
4
+ export type DeepMergeAll<T> = T extends readonly any[] ? T extends [infer First, ...infer Rest] ? Rest extends [] ? First : DeepMerge<First, DeepMergeAll<Rest>> : unknown : T;
5
+ export type IsAny<T, TTrue = true, TFalse = false> = 0 extends (1 & T) ? TTrue : TFalse;
6
+ export type IsEqual<T, U, TTrue = true, TFalse = false> = (<V>() => V extends T ? 1 : 2) extends (<V>() => V extends U ? 1 : 2) ? TTrue : TFalse;
7
+ export type IsJsonType<T, TDeep extends boolean = true, TTrue = true, TFalse = false> = T extends JsonType ? TTrue : T extends Function ? TFalse : T extends Array<infer U> ? TDeep extends false ? TTrue : IsJsonType<U, TDeep, TTrue, TFalse> : T extends object ? TDeep extends false ? TTrue : {
8
+ [K in keyof T]: IsJsonType<T[K], true, false>;
9
+ } extends {
10
+ [K in keyof T]: true;
11
+ } ? TTrue : TFalse : TFalse;
12
+ export type Mutable<T> = {
13
+ -readonly [P in keyof T]: T[P];
14
+ };
15
+ type JsonType = boolean | null | number | string | undefined;
16
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodType, ZodUnion, ZodUnknown } from 'zod/v4';
2
+ export declare function getInnerType(schema: ZodType): undefined | ZodType;
3
+ export declare function isZodArray(obj: unknown): obj is ZodArray<ZodType>;
4
+ export declare function isZodNullable(obj: unknown): obj is ZodNullable<ZodType>;
5
+ export declare function isZodObject(obj: unknown, isLoose?: boolean): obj is ZodObject;
6
+ export declare function isZodOptional(obj: unknown): obj is ZodOptional<ZodType>;
7
+ export declare function isZodType(obj: unknown): obj is ZodType;
8
+ export declare function isZodUnion(obj: unknown): obj is ZodUnion<ZodType[]>;
9
+ export declare function isZodUnknown(obj: unknown): obj is ZodUnknown;
@@ -0,0 +1,41 @@
1
+ import { ZodArray, ZodCatch, ZodDefault, ZodLazy, ZodNullable, ZodOptional } from 'zod/v4';
2
+ export function getInnerType(schema) {
3
+ if (schema instanceof ZodOptional || schema instanceof ZodNullable || schema instanceof ZodDefault || schema instanceof ZodCatch) {
4
+ return schema.def.innerType;
5
+ }
6
+ else if (schema instanceof ZodLazy) {
7
+ return schema.def.getter();
8
+ }
9
+ else if (schema instanceof ZodArray) {
10
+ return schema.element;
11
+ }
12
+ // Not a recognized wrapper type
13
+ return undefined;
14
+ }
15
+ export function isZodArray(obj) {
16
+ return isZodType(obj) && (obj.def.type === 'array');
17
+ }
18
+ export function isZodNullable(obj) {
19
+ return isZodType(obj) && (obj.def.type === 'nullable');
20
+ }
21
+ export function isZodObject(obj, isLoose) {
22
+ if (!isZodType(obj) || (obj.def.type !== 'object')) {
23
+ return false;
24
+ }
25
+ if (typeof isLoose === 'undefined') {
26
+ return true;
27
+ }
28
+ return isLoose === isZodUnknown(obj.def.catchall);
29
+ }
30
+ export function isZodOptional(obj) {
31
+ return isZodType(obj) && (obj.def.type === 'optional');
32
+ }
33
+ export function isZodType(obj) {
34
+ return typeof obj === 'object' && obj !== null && 'def' in obj && 'safeParse' in obj && typeof obj.safeParse === 'function';
35
+ }
36
+ export function isZodUnion(obj) {
37
+ return isZodType(obj) && (obj.def.type === 'union');
38
+ }
39
+ export function isZodUnknown(obj) {
40
+ return isZodType(obj) && (obj.def.type === 'unknown');
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { z } from 'zod/v4';
4
+ import { getInnerType, isZodObject, isZodOptional, isZodUnion, isZodUnknown } from './utilities.js';
5
+ describe('.utilities()', () => {
6
+ it('getInnerType identifies inner types', () => {
7
+ const optionalString = z.string().optional();
8
+ const nullableString = z.string().nullable();
9
+ const defaultString = z.string().default('default');
10
+ const catchString = z.string().catch('fallback');
11
+ const lazyString = z.lazy(() => z.string());
12
+ const arrayString = z.array(z.string());
13
+ assert.strictEqual(getInnerType(optionalString).def.type, optionalString.def.innerType.def.type);
14
+ assert.strictEqual(getInnerType(nullableString).def.type, nullableString.def.innerType.def.type);
15
+ assert.strictEqual(getInnerType(defaultString).def.type, defaultString.def.innerType.def.type);
16
+ assert.strictEqual(getInnerType(catchString).def.type, catchString.def.innerType.def.type);
17
+ assert.strictEqual(getInnerType(lazyString).def.type, lazyString.def.getter().def.type);
18
+ assert.strictEqual(getInnerType(arrayString).def.type, arrayString.element.def.type);
19
+ assert.strictEqual(getInnerType(z.string()), undefined); // Not a wrapper type
20
+ });
21
+ it('isZodObject detects ZodObject', () => {
22
+ const objectSchema = z.object({ name: z.string() });
23
+ const stringSchema = z.string();
24
+ assert.ok(isZodObject(objectSchema));
25
+ assert.ok(!isZodObject(stringSchema));
26
+ });
27
+ it('isZodObject should detect loose objects when isLoose is passed', () => {
28
+ assert.ok(isZodObject(z.looseObject({}), true));
29
+ assert.ok(!isZodObject(z.looseObject({}), false));
30
+ assert.ok(!isZodObject(z.strictObject({}), true));
31
+ assert.ok(isZodObject(z.strictObject({}), false));
32
+ });
33
+ it('isZodOptional detects ZodOptional', () => {
34
+ const optionalSchema = z.string().optional();
35
+ const stringSchema = z.string();
36
+ assert.ok(isZodOptional(optionalSchema));
37
+ assert.ok(!isZodOptional(stringSchema));
38
+ });
39
+ it('isZodUnion detects ZodUnion', () => {
40
+ const unionSchema = z.union([z.string(), z.number()]);
41
+ const stringSchema = z.string();
42
+ assert.ok(isZodUnion(unionSchema));
43
+ assert.ok(!isZodUnion(stringSchema));
44
+ });
45
+ it('isZodUnknown detects ZodUnknown', () => {
46
+ assert.ok(isZodUnknown(z.unknown()));
47
+ assert.ok(!isZodUnknown(z.string()));
48
+ });
49
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "zodselect",
3
+ "version": "1.0.0",
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
+ "author": "SecureDataSystems",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "./dist",
12
+ "package.json",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "devDependencies": {
24
+ "@securedatasystems/eslint-config": "^1.0.2",
25
+ "@securedatasystems/tsconfig": "^1.0.0",
26
+ "@types/node": "^24.0.0",
27
+ "glob": "^11.0.2",
28
+ "tsx": "^4.20.0",
29
+ "typescript": "^5.8.3"
30
+ },
31
+ "dependencies": {
32
+ "zod": "^3.25.58"
33
+ },
34
+ "scripts": {
35
+ "build": "node scripts/build.mjs && tsc --project tsconfig.build.json",
36
+ "deploy": "npm run build && node ./scripts/publish.mjs",
37
+ "lint": "eslint . --c eslint.config.mjs",
38
+ "lint:fix": "eslint . --c eslint.config.mjs --fix",
39
+ "test": "glob -c \"tsx --tsconfig ./tsconfig.build.json --test\" \"./src/**/*.{spec,test}.ts\""
40
+ }
41
+ }