@hy_ong/zod-kit 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -39,12 +39,16 @@ pnpm add @hy_ong/zod-kit zod
39
39
  ```typescript
40
40
  import { email, password, text, mobile, datetime, time, postalCode } from '@hy_ong/zod-kit'
41
41
 
42
- // Simple email validation
43
- const emailSchema = email()
42
+ // Simple email validation (required by default)
43
+ const emailSchema = email(true)
44
44
  emailSchema.parse('user@example.com') // ✅ "user@example.com"
45
45
 
46
+ // Optional email
47
+ const optionalEmail = email(false)
48
+ optionalEmail.parse(null) // ✅ null
49
+
46
50
  // Password with complexity requirements
47
- const passwordSchema = password({
51
+ const passwordSchema = password(true, {
48
52
  minLength: 8,
49
53
  requireUppercase: true,
50
54
  requireDigits: true,
@@ -52,19 +56,19 @@ const passwordSchema = password({
52
56
  })
53
57
 
54
58
  // Taiwan mobile phone validation
55
- const phoneSchema = mobile()
59
+ const phoneSchema = mobile(true)
56
60
  phoneSchema.parse('0912345678') // ✅ "0912345678"
57
61
 
58
62
  // DateTime validation
59
- const datetimeSchema = datetime()
63
+ const datetimeSchema = datetime(true)
60
64
  datetimeSchema.parse('2024-03-15 14:30') // ✅ "2024-03-15 14:30"
61
65
 
62
66
  // Time validation
63
- const timeSchema = time()
67
+ const timeSchema = time(true)
64
68
  timeSchema.parse('14:30') // ✅ "14:30"
65
69
 
66
70
  // Taiwan postal code validation
67
- const postalSchema = postalCode()
71
+ const postalSchema = postalCode(true)
68
72
  postalSchema.parse('100001') // ✅ "100001"
69
73
  ```
70
74
 
@@ -72,37 +76,51 @@ postalSchema.parse('100001') // ✅ "100001"
72
76
 
73
77
  ### Common Validators
74
78
 
75
- #### `email(options?)`
79
+ #### `email(required?, options?)`
76
80
 
77
81
  Validates email addresses with comprehensive format checking.
78
82
 
83
+ **Parameters:**
84
+ - `required` (boolean, optional): Whether the field is required. Default: `false`
85
+ - `options` (object, optional): Configuration options
86
+
79
87
  ```typescript
80
88
  import { email } from '@hy_ong/zod-kit'
81
89
 
82
- // Basic usage
83
- const basicEmail = email()
90
+ // Required email (recommended)
91
+ const requiredEmail = email(true)
92
+
93
+ // Optional email
94
+ const optionalEmail = email(false)
95
+ optionalEmail.parse(null) // ✅ null
84
96
 
85
97
  // With options
86
- const advancedEmail = email({
87
- required: true, // Default: true
98
+ const advancedEmail = email(true, {
88
99
  allowedDomains: ['gmail.com', 'company.com'],
89
100
  minLength: 5,
90
101
  maxLength: 100,
91
102
  transform: (val) => val.toLowerCase(),
103
+ defaultValue: 'default@example.com',
92
104
  i18n: {
93
- en: { invalid: 'Please enter a valid email' }
105
+ en: { invalid: 'Please enter a valid email' },
106
+ 'zh-TW': { invalid: '請輸入有效的電子郵件' }
94
107
  }
95
108
  })
96
109
  ```
97
110
 
98
- #### `password(options?)`
111
+ #### `password(required?, options?)`
99
112
 
100
113
  Validates passwords with customizable complexity requirements.
101
114
 
115
+ **Parameters:**
116
+ - `required` (boolean, optional): Whether the field is required. Default: `false`
117
+ - `options` (object, optional): Configuration options
118
+
102
119
  ```typescript
103
120
  import { password } from '@hy_ong/zod-kit'
104
121
 
105
- const passwordSchema = password({
122
+ // Required password with complexity rules
123
+ const passwordSchema = password(true, {
106
124
  minLength: 8, // Minimum length
107
125
  maxLength: 128, // Maximum length
108
126
  requireUppercase: true, // Require A-Z
@@ -113,67 +131,77 @@ const passwordSchema = password({
113
131
  { pattern: /[A-Z]/, message: 'Need uppercase' }
114
132
  ]
115
133
  })
134
+
135
+ // Optional password
136
+ const optionalPassword = password(false)
116
137
  ```
117
138
 
118
- #### `text(options?)`
139
+ #### `text(required?, options?)`
119
140
 
120
141
  General text validation with length and pattern constraints.
121
142
 
122
143
  ```typescript
123
144
  import { text } from '@hy_ong/zod-kit'
124
145
 
125
- const nameSchema = text({
146
+ const nameSchema = text(true, {
126
147
  minLength: 2,
127
148
  maxLength: 50,
128
149
  pattern: /^[a-zA-Z\s]+$/,
129
150
  transform: (val) => val.trim()
130
151
  })
152
+
153
+ const optionalText = text(false)
131
154
  ```
132
155
 
133
- #### `number(options?)`
156
+ #### `number(required?, options?)`
134
157
 
135
158
  Validates numeric values with range and type constraints.
136
159
 
137
160
  ```typescript
138
161
  import { number } from '@hy_ong/zod-kit'
139
162
 
140
- const ageSchema = number({
163
+ const ageSchema = number(true, {
141
164
  min: 0,
142
165
  max: 150,
143
166
  integer: true,
144
167
  positive: true
145
168
  })
169
+
170
+ const optionalNumber = number(false)
146
171
  ```
147
172
 
148
- #### `url(options?)`
173
+ #### `url(required?, options?)`
149
174
 
150
175
  URL validation with protocol and domain restrictions.
151
176
 
152
177
  ```typescript
153
178
  import { url } from '@hy_ong/zod-kit'
154
179
 
155
- const urlSchema = url({
180
+ const urlSchema = url(true, {
156
181
  protocols: ['https'], // Only HTTPS allowed
157
182
  allowedDomains: ['safe.com'], // Domain whitelist
158
183
  requireTLD: true // Require top-level domain
159
184
  })
185
+
186
+ const optionalUrl = url(false)
160
187
  ```
161
188
 
162
- #### `boolean(options?)`
189
+ #### `boolean(required?, options?)`
163
190
 
164
191
  Boolean validation with flexible input handling.
165
192
 
166
193
  ```typescript
167
194
  import { boolean } from '@hy_ong/zod-kit'
168
195
 
169
- const consentSchema = boolean({
170
- required: true,
196
+ const consentSchema = boolean(true, {
171
197
  trueValues: ['yes', '1', 'true'], // Custom truthy values
172
198
  falseValues: ['no', '0', 'false'] // Custom falsy values
173
199
  })
200
+
201
+ const optionalBoolean = boolean(false)
174
202
  ```
175
203
 
176
- #### `datetime(options?)`
204
+ #### `datetime(required?, options?)`
177
205
 
178
206
  Validates datetime with comprehensive format support and timezone handling.
179
207
 
@@ -181,11 +209,11 @@ Validates datetime with comprehensive format support and timezone handling.
181
209
  import { datetime } from '@hy_ong/zod-kit'
182
210
 
183
211
  // Basic datetime validation
184
- const basicSchema = datetime()
212
+ const basicSchema = datetime(true)
185
213
  basicSchema.parse('2024-03-15 14:30') // ✓ Valid
186
214
 
187
215
  // Business hours validation
188
- const businessHours = datetime({
216
+ const businessHours = datetime(true, {
189
217
  format: 'YYYY-MM-DD HH:mm',
190
218
  minHour: 9,
191
219
  maxHour: 17,
@@ -193,19 +221,22 @@ const businessHours = datetime({
193
221
  })
194
222
 
195
223
  // Timezone-aware validation
196
- const timezoneSchema = datetime({
224
+ const timezoneSchema = datetime(true, {
197
225
  timezone: 'Asia/Taipei',
198
226
  mustBeFuture: true
199
227
  })
200
228
 
201
229
  // Multiple format support
202
- const flexibleSchema = datetime({
230
+ const flexibleSchema = datetime(true, {
203
231
  format: 'DD/MM/YYYY HH:mm'
204
232
  })
205
233
  flexibleSchema.parse('15/03/2024 14:30') // ✓ Valid
234
+
235
+ // Optional datetime
236
+ const optionalDatetime = datetime(false)
206
237
  ```
207
238
 
208
- #### `time(options?)`
239
+ #### `time(required?, options?)`
209
240
 
210
241
  Time validation with multiple formats and constraints.
211
242
 
@@ -213,15 +244,15 @@ Time validation with multiple formats and constraints.
213
244
  import { time } from '@hy_ong/zod-kit'
214
245
 
215
246
  // Basic time validation (24-hour format)
216
- const basicSchema = time()
247
+ const basicSchema = time(true)
217
248
  basicSchema.parse('14:30') // ✓ Valid
218
249
 
219
250
  // 12-hour format with AM/PM
220
- const ampmSchema = time({ format: 'hh:mm A' })
251
+ const ampmSchema = time(true, { format: 'hh:mm A' })
221
252
  ampmSchema.parse('02:30 PM') // ✓ Valid
222
253
 
223
254
  // Business hours validation
224
- const businessHours = time({
255
+ const businessHours = time(true, {
225
256
  format: 'HH:mm',
226
257
  minHour: 9,
227
258
  maxHour: 17,
@@ -229,28 +260,33 @@ const businessHours = time({
229
260
  })
230
261
 
231
262
  // Time range validation
232
- const timeRangeSchema = time({
263
+ const timeRangeSchema = time(true, {
233
264
  min: '09:00',
234
265
  max: '17:00'
235
266
  })
267
+
268
+ // Optional time
269
+ const optionalTime = time(false)
236
270
  ```
237
271
 
238
- #### `date(options?)`
272
+ #### `date(required?, options?)`
239
273
 
240
274
  Date validation with range and format constraints.
241
275
 
242
276
  ```typescript
243
277
  import { date } from '@hy_ong/zod-kit'
244
278
 
245
- const birthdateSchema = date({
279
+ const birthdateSchema = date(true, {
246
280
  format: 'YYYY-MM-DD',
247
281
  minDate: '1900-01-01',
248
282
  maxDate: new Date(),
249
283
  timezone: 'Asia/Taipei'
250
284
  })
285
+
286
+ const optionalDate = date(false)
251
287
  ```
252
288
 
253
- #### `file(options?)`
289
+ #### `file(required?, options?)`
254
290
 
255
291
  File validation with MIME type filtering and size constraints.
256
292
 
@@ -258,113 +294,127 @@ File validation with MIME type filtering and size constraints.
258
294
  import { file } from '@hy_ong/zod-kit'
259
295
 
260
296
  // Basic file validation
261
- const basicSchema = file()
297
+ const basicSchema = file(true)
262
298
  basicSchema.parse(new File(['content'], 'test.txt'))
263
299
 
264
300
  // Size restrictions
265
- const sizeSchema = file({
301
+ const sizeSchema = file(true, {
266
302
  maxSize: 1024 * 1024, // 1MB
267
303
  minSize: 1024 // 1KB
268
304
  })
269
305
 
270
306
  // Extension restrictions
271
- const imageSchema = file({
307
+ const imageSchema = file(true, {
272
308
  extension: ['.jpg', '.png', '.gif'],
273
309
  maxSize: 5 * 1024 * 1024 // 5MB
274
310
  })
275
311
 
276
312
  // MIME type restrictions
277
- const documentSchema = file({
313
+ const documentSchema = file(true, {
278
314
  type: ['application/pdf', 'application/msword'],
279
315
  maxSize: 10 * 1024 * 1024 // 10MB
280
316
  })
281
317
 
282
318
  // Image files only
283
- const imageOnlySchema = file({ imageOnly: true })
319
+ const imageOnlySchema = file(true, { imageOnly: true })
320
+
321
+ // Optional file
322
+ const optionalFile = file(false)
284
323
  ```
285
324
 
286
- #### `id(options?)`
325
+ #### `id(required?, options?)`
287
326
 
288
327
  Flexible ID validation supporting multiple formats.
289
328
 
290
329
  ```typescript
291
330
  import { id } from '@hy_ong/zod-kit'
292
331
 
293
- const userIdSchema = id({
332
+ const userIdSchema = id(true, {
294
333
  type: 'uuid', // 'uuid', 'nanoid', 'objectId', 'auto', etc.
295
334
  allowedTypes: ['uuid', 'nanoid'], // Multiple allowed types
296
335
  customRegex: /^USR_[A-Z0-9]+$/ // Custom pattern
297
336
  })
337
+
338
+ const optionalId = id(false)
298
339
  ```
299
340
 
300
341
  ### Taiwan-Specific Validators
301
342
 
302
- #### `nationalId(options?)`
343
+ #### `nationalId(required?, options?)`
303
344
 
304
345
  Validates Taiwan National ID (身份證字號).
305
346
 
306
347
  ```typescript
307
348
  import { nationalId } from '@hy_ong/zod-kit'
308
349
 
309
- const idSchema = nationalId({
310
- required: true,
350
+ const idSchema = nationalId(true, {
311
351
  normalize: true, // Convert to uppercase
312
352
  whitelist: ['A123456789'] // Allow specific IDs
313
353
  })
314
354
 
315
355
  idSchema.parse('A123456789') // ✅ Valid Taiwan National ID
356
+
357
+ const optionalId = nationalId(false)
316
358
  ```
317
359
 
318
- #### `businessId(options?)`
360
+ #### `businessId(required?, options?)`
319
361
 
320
362
  Validates Taiwan Business ID (統一編號).
321
363
 
322
364
  ```typescript
323
365
  import { businessId } from '@hy_ong/zod-kit'
324
366
 
325
- const bizSchema = businessId()
367
+ const bizSchema = businessId(true)
326
368
  bizSchema.parse('12345675') // ✅ Valid business ID with checksum
369
+
370
+ const optionalBizId = businessId(false)
327
371
  ```
328
372
 
329
- #### `mobile(options?)`
373
+ #### `mobile(required?, options?)`
330
374
 
331
375
  Validates Taiwan mobile phone numbers.
332
376
 
333
377
  ```typescript
334
378
  import { mobile } from '@hy_ong/zod-kit'
335
379
 
336
- const phoneSchema = mobile({
380
+ const phoneSchema = mobile(true, {
337
381
  allowInternational: true, // Allow +886 prefix
338
382
  allowSeparators: true, // Allow 0912-345-678
339
383
  operators: ['09'] // Restrict to specific operators
340
384
  })
385
+
386
+ const optionalMobile = mobile(false)
341
387
  ```
342
388
 
343
- #### `tel(options?)`
389
+ #### `tel(required?, options?)`
344
390
 
345
391
  Validates Taiwan landline telephone numbers.
346
392
 
347
393
  ```typescript
348
394
  import { tel } from '@hy_ong/zod-kit'
349
395
 
350
- const landlineSchema = tel({
396
+ const landlineSchema = tel(true, {
351
397
  allowSeparators: true, // Allow 02-1234-5678
352
398
  areaCodes: ['02', '03'] // Restrict to specific areas
353
399
  })
400
+
401
+ const optionalTel = tel(false)
354
402
  ```
355
403
 
356
- #### `fax(options?)`
404
+ #### `fax(required?, options?)`
357
405
 
358
406
  Validates Taiwan fax numbers (same format as landline).
359
407
 
360
408
  ```typescript
361
409
  import { fax } from '@hy_ong/zod-kit'
362
410
 
363
- const faxSchema = fax()
411
+ const faxSchema = fax(true)
364
412
  faxSchema.parse('02-2345-6789') // ✅ Valid fax number
413
+
414
+ const optionalFax = fax(false)
365
415
  ```
366
416
 
367
- #### `postalCode(options?)`
417
+ #### `postalCode(required?, options?)`
368
418
 
369
419
  Validates Taiwan postal codes with support for 3-digit, 5-digit, and 6-digit formats.
370
420
 
@@ -372,31 +422,34 @@ Validates Taiwan postal codes with support for 3-digit, 5-digit, and 6-digit for
372
422
  import { postalCode } from '@hy_ong/zod-kit'
373
423
 
374
424
  // Accept 3-digit or 6-digit formats (recommended)
375
- const modernSchema = postalCode()
425
+ const modernSchema = postalCode(true)
376
426
  modernSchema.parse('100') // ✅ Valid 3-digit
377
427
  modernSchema.parse('100001') // ✅ Valid 6-digit
378
428
 
379
429
  // Accept all formats
380
- const flexibleSchema = postalCode({ format: 'all' })
430
+ const flexibleSchema = postalCode(true, { format: 'all' })
381
431
  flexibleSchema.parse('100') // ✅ Valid
382
432
  flexibleSchema.parse('10001') // ✅ Valid (5-digit legacy)
383
433
  flexibleSchema.parse('100001') // ✅ Valid
384
434
 
385
435
  // Only 6-digit format (current standard)
386
- const modernOnlySchema = postalCode({ format: '6' })
436
+ const modernOnlySchema = postalCode(true, { format: '6' })
387
437
  modernOnlySchema.parse('100001') // ✅ Valid
388
438
  modernOnlySchema.parse('100') // ❌ Invalid
389
439
 
390
440
  // With dashes allowed
391
- const dashSchema = postalCode({ allowDashes: true })
441
+ const dashSchema = postalCode(true, { allowDashes: true })
392
442
  dashSchema.parse('100-001') // ✅ Valid (normalized to '100001')
393
443
 
394
444
  // Specific areas only
395
- const taipeiSchema = postalCode({
445
+ const taipeiSchema = postalCode(true, {
396
446
  allowedPrefixes: ['100', '103', '104', '105', '106']
397
447
  })
398
448
  taipeiSchema.parse('100001') // ✅ Valid (Taipei area)
399
449
  taipeiSchema.parse('200001') // ❌ Invalid (not in allowlist)
450
+
451
+ // Optional postal code
452
+ const optionalPostal = postalCode(false)
400
453
  ```
401
454
 
402
455
  ## 🌐 Internationalization
@@ -411,7 +464,7 @@ setLocale('zh-TW') // Traditional Chinese
411
464
  setLocale('en') // English (default)
412
465
 
413
466
  // Or use custom messages per validator
414
- const emailSchema = email({
467
+ const emailSchema = email(true, {
415
468
  i18n: {
416
469
  en: {
417
470
  required: 'Email is required',
@@ -429,10 +482,10 @@ const emailSchema = email({
429
482
 
430
483
  ### Optional Fields
431
484
 
432
- Make any field optional by setting `required: false`:
485
+ Make any field optional by passing `false` as the first argument:
433
486
 
434
487
  ```typescript
435
- const optionalEmail = email({ required: false })
488
+ const optionalEmail = email(false)
436
489
 
437
490
  optionalEmail.parse(null) // ✅ null
438
491
  optionalEmail.parse('') // ✅ null
@@ -444,7 +497,7 @@ optionalEmail.parse('test@example.com') // ✅ "test@example.com"
444
497
  Transform values during validation:
445
498
 
446
499
  ```typescript
447
- const trimmedText = text({
500
+ const trimmedText = text(true, {
448
501
  transform: (val) => val.trim().toLowerCase(),
449
502
  minLength: 1
450
503
  })
@@ -452,12 +505,25 @@ const trimmedText = text({
452
505
  trimmedText.parse(' HELLO ') // ✅ "hello"
453
506
  ```
454
507
 
508
+ ### Default Values
509
+
510
+ Provide default values for empty inputs:
511
+
512
+ ```typescript
513
+ const emailWithDefault = email(true, {
514
+ defaultValue: 'default@example.com'
515
+ })
516
+
517
+ emailWithDefault.parse('') // ✅ "default@example.com"
518
+ emailWithDefault.parse(null) // ✅ "default@example.com"
519
+ ```
520
+
455
521
  ### Whitelist Validation
456
522
 
457
523
  Allow specific values regardless of format:
458
524
 
459
525
  ```typescript
460
- const flexibleId = id({
526
+ const flexibleId = id(true, {
461
527
  type: 'uuid',
462
528
  whitelist: ['admin', 'system', 'test-user']
463
529
  })
@@ -475,8 +541,8 @@ import { z } from 'zod'
475
541
  import { email, password } from '@hy_ong/zod-kit'
476
542
 
477
543
  const userSchema = z.object({
478
- email: email(),
479
- password: password({ minLength: 8 }),
544
+ email: email(true),
545
+ password: password(true, { minLength: 8 }),
480
546
  confirmPassword: z.string()
481
547
  }).refine(data => data.password === data.confirmPassword, {
482
548
  message: "Passwords don't match"
package/dist/index.cjs CHANGED
@@ -1291,35 +1291,31 @@ var validateIdType = (value, type) => {
1291
1291
  return pattern ? pattern.test(value) : false;
1292
1292
  };
1293
1293
  function id(required, options) {
1294
- const {
1295
- type = "auto",
1296
- minLength,
1297
- maxLength,
1298
- allowedTypes,
1299
- customRegex,
1300
- includes,
1301
- excludes,
1302
- startsWith,
1303
- endsWith,
1304
- caseSensitive = true,
1305
- transform,
1306
- defaultValue,
1307
- i18n
1308
- } = options ?? {};
1294
+ const { type = "auto", minLength, maxLength, allowedTypes, customRegex, includes, excludes, startsWith, endsWith, caseSensitive = true, transform, defaultValue, i18n } = options ?? {};
1309
1295
  const isRequired = required ?? false;
1310
- const actualDefaultValue = defaultValue ?? (isRequired ? "" : null);
1296
+ const isNumericType = type === "numeric";
1297
+ const actualDefaultValue = defaultValue !== void 0 ? defaultValue : isRequired ? isNumericType ? NaN : "" : null;
1311
1298
  const getMessage = (key, params) => {
1312
1299
  if (i18n) {
1313
1300
  const currentLocale2 = getLocale();
1314
1301
  const customMessages = i18n[currentLocale2];
1315
1302
  if (customMessages && customMessages[key]) {
1316
1303
  const template = customMessages[key];
1317
- return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "");
1304
+ return template.replace(/\$\{(\w+)}/g, (_match, k) => params?.[k] ?? "");
1318
1305
  }
1319
1306
  }
1320
1307
  return t(`common.id.${key}`, params);
1321
1308
  };
1322
- const preprocessFn = (val) => {
1309
+ const preprocessNumericFn = (val) => {
1310
+ if (val === "" || val === null || val === void 0) {
1311
+ if (isRequired && defaultValue === void 0) {
1312
+ return void 0;
1313
+ }
1314
+ return actualDefaultValue;
1315
+ }
1316
+ return Number(val);
1317
+ };
1318
+ const preprocessStringFn = (val) => {
1323
1319
  if (val === "" || val === null || val === void 0) {
1324
1320
  return actualDefaultValue;
1325
1321
  }
@@ -1329,7 +1325,30 @@ function id(required, options) {
1329
1325
  }
1330
1326
  return processed;
1331
1327
  };
1332
- const baseSchema = isRequired ? import_zod6.z.preprocess(preprocessFn, import_zod6.z.string()) : import_zod6.z.preprocess(preprocessFn, import_zod6.z.string().nullable());
1328
+ if (isNumericType) {
1329
+ const numericSchema = import_zod6.z.preprocess(preprocessNumericFn, import_zod6.z.any()).refine((val) => {
1330
+ if (!isRequired && val === null) return true;
1331
+ if (val === void 0 || isRequired && val === null) {
1332
+ throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }]);
1333
+ }
1334
+ if (typeof val !== "number" || isNaN(val)) {
1335
+ throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("numeric"), path: [] }]);
1336
+ }
1337
+ const strVal = String(val);
1338
+ if (!ID_PATTERNS.numeric.test(strVal)) {
1339
+ throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("numeric"), path: [] }]);
1340
+ }
1341
+ if (minLength !== void 0 && strVal.length < minLength) {
1342
+ throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }]);
1343
+ }
1344
+ if (maxLength !== void 0 && strVal.length > maxLength) {
1345
+ throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }]);
1346
+ }
1347
+ return true;
1348
+ });
1349
+ return numericSchema;
1350
+ }
1351
+ const baseSchema = isRequired ? import_zod6.z.preprocess(preprocessStringFn, import_zod6.z.string()) : import_zod6.z.preprocess(preprocessStringFn, import_zod6.z.string().nullable());
1333
1352
  const schema = baseSchema.refine((val) => {
1334
1353
  if (val === null) return true;
1335
1354
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
@@ -1355,7 +1374,7 @@ function id(required, options) {
1355
1374
  const typeNames = allowedTypes.join(", ");
1356
1375
  throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }]);
1357
1376
  }
1358
- } else if (type !== "auto") {
1377
+ } else if (type && type !== "auto") {
1359
1378
  isValidId = validateIdType(val, type);
1360
1379
  if (!isValidId) {
1361
1380
  throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage(type) || getMessage("invalid"), path: [] }]);
@@ -1366,7 +1385,7 @@ function id(required, options) {
1366
1385
  throw new import_zod6.z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }]);
1367
1386
  }
1368
1387
  }
1369
- } else if (val !== null && hasContentValidations && type !== "auto" && !customRegex) {
1388
+ } else if (val !== null && hasContentValidations && type && type !== "auto" && !customRegex) {
1370
1389
  if (allowedTypes && allowedTypes.length > 0) {
1371
1390
  const isValidType = allowedTypes.some((allowedType) => validateIdType(val, allowedType));
1372
1391
  if (!isValidType) {
package/dist/index.d.cts CHANGED
@@ -978,10 +978,11 @@ type IdType = "numeric" | "uuid" | "objectId" | "nanoid" | "snowflake" | "cuid"
978
978
  * Configuration options for ID validation
979
979
  *
980
980
  * @template IsRequired - Whether the field is required (affects return type)
981
+ * @template Type - The ID type being validated
981
982
  *
982
983
  * @interface IdOptions
983
984
  * @property {IsRequired} [required=true] - Whether the field is required
984
- * @property {IdType} [type="auto"] - Expected ID type or auto-detection
985
+ * @property {Type} [type="auto"] - Expected ID type or auto-detection
985
986
  * @property {number} [minLength] - Minimum length of ID
986
987
  * @property {number} [maxLength] - Maximum length of ID
987
988
  * @property {IdType[]} [allowedTypes] - Multiple allowed ID types (overrides type)
@@ -992,11 +993,11 @@ type IdType = "numeric" | "uuid" | "objectId" | "nanoid" | "snowflake" | "cuid"
992
993
  * @property {string} [endsWith] - String that ID must end with
993
994
  * @property {boolean} [caseSensitive=true] - Whether validation is case-sensitive
994
995
  * @property {Function} [transform] - Custom transformation function for ID
995
- * @property {string | null} [defaultValue] - Default value when input is empty
996
+ * @property {any} [defaultValue] - Default value when input is empty (string for string types, number for numeric)
996
997
  * @property {Record<Locale, IdMessages>} [i18n] - Custom error messages for different locales
997
998
  */
998
- type IdOptions<IsRequired extends boolean = true> = {
999
- type?: IdType;
999
+ type IdOptions<Type extends IdType | undefined = undefined> = {
1000
+ type?: Type;
1000
1001
  minLength?: number;
1001
1002
  maxLength?: number;
1002
1003
  allowedTypes?: IdType[];
@@ -1007,17 +1008,20 @@ type IdOptions<IsRequired extends boolean = true> = {
1007
1008
  endsWith?: string;
1008
1009
  caseSensitive?: boolean;
1009
1010
  transform?: (value: string) => string;
1010
- defaultValue?: IsRequired extends true ? string : string | null;
1011
+ defaultValue?: any;
1011
1012
  i18n?: Record<Locale, IdMessages>;
1012
1013
  };
1013
1014
  /**
1014
- * Type alias for ID validation schema based on required flag
1015
+ * Type alias for ID validation schema based on required flag and ID type
1015
1016
  *
1016
1017
  * @template IsRequired - Whether the field is required
1018
+ * @template IdType - The ID type being validated
1017
1019
  * @typedef IdSchema
1018
- * @description Returns ZodString if required, ZodNullable<ZodString> if optional
1020
+ * @description Returns appropriate Zod type based on required flag and ID type:
1021
+ * - numeric type: ZodNumber or ZodNullable<ZodNumber>
1022
+ * - other types: ZodString or ZodNullable<ZodString>
1019
1023
  */
1020
- type IdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>;
1024
+ type IdSchema<IsRequired extends boolean, Type extends IdType | undefined = undefined> = Type extends "numeric" ? IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber> : IsRequired extends true ? ZodString : ZodNullable<ZodString>;
1021
1025
  /**
1022
1026
  * Regular expression patterns for different ID formats
1023
1027
  *
@@ -1079,9 +1083,9 @@ declare const validateIdType: (value: string, type: IdType) => boolean;
1079
1083
  * Creates a Zod schema for ID validation with comprehensive format support
1080
1084
  *
1081
1085
  * @template IsRequired - Whether the field is required (affects return type)
1086
+ * @template Type - The ID type being validated (affects return type for numeric)
1082
1087
  * @param {IsRequired} [required=false] - Whether the field is required
1083
- * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
1084
- * @returns {IdSchema<IsRequired>} Zod schema for ID validation
1088
+ * @returns {IdSchema<IsRequired, Type>} Zod schema for ID validation
1085
1089
  *
1086
1090
  * @description
1087
1091
  * Creates a comprehensive ID validator with support for multiple ID formats,
@@ -1149,7 +1153,16 @@ declare const validateIdType: (value: string, type: IdType) => boolean;
1149
1153
  * @see {@link detectIdType} for auto-detection logic
1150
1154
  * @see {@link validateIdType} for type-specific validation
1151
1155
  */
1152
- declare function id<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<IdOptions<IsRequired>, 'required'>): IdSchema<IsRequired>;
1156
+ declare function id<IsRequired extends boolean = false>(required?: IsRequired): IdSchema<IsRequired, undefined>;
1157
+ declare function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions<"numeric">, "required"> & {
1158
+ type: "numeric";
1159
+ }): IdSchema<IsRequired, "numeric">;
1160
+ declare function id<IsRequired extends boolean = false, Type extends Exclude<IdType, "numeric"> = Exclude<IdType, "numeric">>(required: IsRequired, options: Omit<IdOptions<Type>, "required"> & {
1161
+ type: Type;
1162
+ }): IdSchema<IsRequired, Type>;
1163
+ declare function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions, "required"> & {
1164
+ type?: never;
1165
+ }): IdSchema<IsRequired, undefined>;
1153
1166
 
1154
1167
  /**
1155
1168
  * @fileoverview Number validator for Zod Kit
package/dist/index.d.ts CHANGED
@@ -978,10 +978,11 @@ type IdType = "numeric" | "uuid" | "objectId" | "nanoid" | "snowflake" | "cuid"
978
978
  * Configuration options for ID validation
979
979
  *
980
980
  * @template IsRequired - Whether the field is required (affects return type)
981
+ * @template Type - The ID type being validated
981
982
  *
982
983
  * @interface IdOptions
983
984
  * @property {IsRequired} [required=true] - Whether the field is required
984
- * @property {IdType} [type="auto"] - Expected ID type or auto-detection
985
+ * @property {Type} [type="auto"] - Expected ID type or auto-detection
985
986
  * @property {number} [minLength] - Minimum length of ID
986
987
  * @property {number} [maxLength] - Maximum length of ID
987
988
  * @property {IdType[]} [allowedTypes] - Multiple allowed ID types (overrides type)
@@ -992,11 +993,11 @@ type IdType = "numeric" | "uuid" | "objectId" | "nanoid" | "snowflake" | "cuid"
992
993
  * @property {string} [endsWith] - String that ID must end with
993
994
  * @property {boolean} [caseSensitive=true] - Whether validation is case-sensitive
994
995
  * @property {Function} [transform] - Custom transformation function for ID
995
- * @property {string | null} [defaultValue] - Default value when input is empty
996
+ * @property {any} [defaultValue] - Default value when input is empty (string for string types, number for numeric)
996
997
  * @property {Record<Locale, IdMessages>} [i18n] - Custom error messages for different locales
997
998
  */
998
- type IdOptions<IsRequired extends boolean = true> = {
999
- type?: IdType;
999
+ type IdOptions<Type extends IdType | undefined = undefined> = {
1000
+ type?: Type;
1000
1001
  minLength?: number;
1001
1002
  maxLength?: number;
1002
1003
  allowedTypes?: IdType[];
@@ -1007,17 +1008,20 @@ type IdOptions<IsRequired extends boolean = true> = {
1007
1008
  endsWith?: string;
1008
1009
  caseSensitive?: boolean;
1009
1010
  transform?: (value: string) => string;
1010
- defaultValue?: IsRequired extends true ? string : string | null;
1011
+ defaultValue?: any;
1011
1012
  i18n?: Record<Locale, IdMessages>;
1012
1013
  };
1013
1014
  /**
1014
- * Type alias for ID validation schema based on required flag
1015
+ * Type alias for ID validation schema based on required flag and ID type
1015
1016
  *
1016
1017
  * @template IsRequired - Whether the field is required
1018
+ * @template IdType - The ID type being validated
1017
1019
  * @typedef IdSchema
1018
- * @description Returns ZodString if required, ZodNullable<ZodString> if optional
1020
+ * @description Returns appropriate Zod type based on required flag and ID type:
1021
+ * - numeric type: ZodNumber or ZodNullable<ZodNumber>
1022
+ * - other types: ZodString or ZodNullable<ZodString>
1019
1023
  */
1020
- type IdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>;
1024
+ type IdSchema<IsRequired extends boolean, Type extends IdType | undefined = undefined> = Type extends "numeric" ? IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber> : IsRequired extends true ? ZodString : ZodNullable<ZodString>;
1021
1025
  /**
1022
1026
  * Regular expression patterns for different ID formats
1023
1027
  *
@@ -1079,9 +1083,9 @@ declare const validateIdType: (value: string, type: IdType) => boolean;
1079
1083
  * Creates a Zod schema for ID validation with comprehensive format support
1080
1084
  *
1081
1085
  * @template IsRequired - Whether the field is required (affects return type)
1086
+ * @template Type - The ID type being validated (affects return type for numeric)
1082
1087
  * @param {IsRequired} [required=false] - Whether the field is required
1083
- * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
1084
- * @returns {IdSchema<IsRequired>} Zod schema for ID validation
1088
+ * @returns {IdSchema<IsRequired, Type>} Zod schema for ID validation
1085
1089
  *
1086
1090
  * @description
1087
1091
  * Creates a comprehensive ID validator with support for multiple ID formats,
@@ -1149,7 +1153,16 @@ declare const validateIdType: (value: string, type: IdType) => boolean;
1149
1153
  * @see {@link detectIdType} for auto-detection logic
1150
1154
  * @see {@link validateIdType} for type-specific validation
1151
1155
  */
1152
- declare function id<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<IdOptions<IsRequired>, 'required'>): IdSchema<IsRequired>;
1156
+ declare function id<IsRequired extends boolean = false>(required?: IsRequired): IdSchema<IsRequired, undefined>;
1157
+ declare function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions<"numeric">, "required"> & {
1158
+ type: "numeric";
1159
+ }): IdSchema<IsRequired, "numeric">;
1160
+ declare function id<IsRequired extends boolean = false, Type extends Exclude<IdType, "numeric"> = Exclude<IdType, "numeric">>(required: IsRequired, options: Omit<IdOptions<Type>, "required"> & {
1161
+ type: Type;
1162
+ }): IdSchema<IsRequired, Type>;
1163
+ declare function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions, "required"> & {
1164
+ type?: never;
1165
+ }): IdSchema<IsRequired, undefined>;
1153
1166
 
1154
1167
  /**
1155
1168
  * @fileoverview Number validator for Zod Kit
package/dist/index.js CHANGED
@@ -1213,35 +1213,31 @@ var validateIdType = (value, type) => {
1213
1213
  return pattern ? pattern.test(value) : false;
1214
1214
  };
1215
1215
  function id(required, options) {
1216
- const {
1217
- type = "auto",
1218
- minLength,
1219
- maxLength,
1220
- allowedTypes,
1221
- customRegex,
1222
- includes,
1223
- excludes,
1224
- startsWith,
1225
- endsWith,
1226
- caseSensitive = true,
1227
- transform,
1228
- defaultValue,
1229
- i18n
1230
- } = options ?? {};
1216
+ const { type = "auto", minLength, maxLength, allowedTypes, customRegex, includes, excludes, startsWith, endsWith, caseSensitive = true, transform, defaultValue, i18n } = options ?? {};
1231
1217
  const isRequired = required ?? false;
1232
- const actualDefaultValue = defaultValue ?? (isRequired ? "" : null);
1218
+ const isNumericType = type === "numeric";
1219
+ const actualDefaultValue = defaultValue !== void 0 ? defaultValue : isRequired ? isNumericType ? NaN : "" : null;
1233
1220
  const getMessage = (key, params) => {
1234
1221
  if (i18n) {
1235
1222
  const currentLocale2 = getLocale();
1236
1223
  const customMessages = i18n[currentLocale2];
1237
1224
  if (customMessages && customMessages[key]) {
1238
1225
  const template = customMessages[key];
1239
- return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "");
1226
+ return template.replace(/\$\{(\w+)}/g, (_match, k) => params?.[k] ?? "");
1240
1227
  }
1241
1228
  }
1242
1229
  return t(`common.id.${key}`, params);
1243
1230
  };
1244
- const preprocessFn = (val) => {
1231
+ const preprocessNumericFn = (val) => {
1232
+ if (val === "" || val === null || val === void 0) {
1233
+ if (isRequired && defaultValue === void 0) {
1234
+ return void 0;
1235
+ }
1236
+ return actualDefaultValue;
1237
+ }
1238
+ return Number(val);
1239
+ };
1240
+ const preprocessStringFn = (val) => {
1245
1241
  if (val === "" || val === null || val === void 0) {
1246
1242
  return actualDefaultValue;
1247
1243
  }
@@ -1251,7 +1247,30 @@ function id(required, options) {
1251
1247
  }
1252
1248
  return processed;
1253
1249
  };
1254
- const baseSchema = isRequired ? z6.preprocess(preprocessFn, z6.string()) : z6.preprocess(preprocessFn, z6.string().nullable());
1250
+ if (isNumericType) {
1251
+ const numericSchema = z6.preprocess(preprocessNumericFn, z6.any()).refine((val) => {
1252
+ if (!isRequired && val === null) return true;
1253
+ if (val === void 0 || isRequired && val === null) {
1254
+ throw new z6.ZodError([{ code: "custom", message: getMessage("required"), path: [] }]);
1255
+ }
1256
+ if (typeof val !== "number" || isNaN(val)) {
1257
+ throw new z6.ZodError([{ code: "custom", message: getMessage("numeric"), path: [] }]);
1258
+ }
1259
+ const strVal = String(val);
1260
+ if (!ID_PATTERNS.numeric.test(strVal)) {
1261
+ throw new z6.ZodError([{ code: "custom", message: getMessage("numeric"), path: [] }]);
1262
+ }
1263
+ if (minLength !== void 0 && strVal.length < minLength) {
1264
+ throw new z6.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }]);
1265
+ }
1266
+ if (maxLength !== void 0 && strVal.length > maxLength) {
1267
+ throw new z6.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }]);
1268
+ }
1269
+ return true;
1270
+ });
1271
+ return numericSchema;
1272
+ }
1273
+ const baseSchema = isRequired ? z6.preprocess(preprocessStringFn, z6.string()) : z6.preprocess(preprocessStringFn, z6.string().nullable());
1255
1274
  const schema = baseSchema.refine((val) => {
1256
1275
  if (val === null) return true;
1257
1276
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
@@ -1277,7 +1296,7 @@ function id(required, options) {
1277
1296
  const typeNames = allowedTypes.join(", ");
1278
1297
  throw new z6.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }]);
1279
1298
  }
1280
- } else if (type !== "auto") {
1299
+ } else if (type && type !== "auto") {
1281
1300
  isValidId = validateIdType(val, type);
1282
1301
  if (!isValidId) {
1283
1302
  throw new z6.ZodError([{ code: "custom", message: getMessage(type) || getMessage("invalid"), path: [] }]);
@@ -1288,7 +1307,7 @@ function id(required, options) {
1288
1307
  throw new z6.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }]);
1289
1308
  }
1290
1309
  }
1291
- } else if (val !== null && hasContentValidations && type !== "auto" && !customRegex) {
1310
+ } else if (val !== null && hasContentValidations && type && type !== "auto" && !customRegex) {
1292
1311
  if (allowedTypes && allowedTypes.length > 0) {
1293
1312
  const isValidType = allowedTypes.some((allowedType) => validateIdType(val, allowedType));
1294
1313
  if (!isValidType) {
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@hy_ong/zod-kit",
3
- "version": "0.1.0",
4
- "description": "Zod Kit",
3
+ "version": "0.1.2",
4
+ "description": "A comprehensive TypeScript library providing pre-built Zod validation schemas with full internationalization support for common data types and Taiwan-specific formats",
5
5
  "keywords": [
6
- "hy_ong",
7
6
  "zod",
8
- "kit",
9
- "zod-kit"
7
+ "validation",
8
+ "schema",
9
+ "typescript",
10
+ "taiwan",
11
+ "i18n",
12
+ "form-validation",
13
+ "email",
14
+ "password",
15
+ "phone",
16
+ "postal-code",
17
+ "national-id",
18
+ "business-id"
10
19
  ],
11
20
  "homepage": "https://github.com/hy-ong/zod-kit#readme",
12
21
  "bugs": {
@@ -8,7 +8,7 @@
8
8
  * @version 0.0.5
9
9
  */
10
10
 
11
- import { z, ZodNullable, ZodString } from "zod"
11
+ import { z, ZodNullable, ZodString, ZodNumber } from "zod"
12
12
  import { t } from "../../i18n"
13
13
  import { getLocale, type Locale } from "../../config"
14
14
 
@@ -85,10 +85,11 @@ export type IdType =
85
85
  * Configuration options for ID validation
86
86
  *
87
87
  * @template IsRequired - Whether the field is required (affects return type)
88
+ * @template Type - The ID type being validated
88
89
  *
89
90
  * @interface IdOptions
90
91
  * @property {IsRequired} [required=true] - Whether the field is required
91
- * @property {IdType} [type="auto"] - Expected ID type or auto-detection
92
+ * @property {Type} [type="auto"] - Expected ID type or auto-detection
92
93
  * @property {number} [minLength] - Minimum length of ID
93
94
  * @property {number} [maxLength] - Maximum length of ID
94
95
  * @property {IdType[]} [allowedTypes] - Multiple allowed ID types (overrides type)
@@ -99,11 +100,11 @@ export type IdType =
99
100
  * @property {string} [endsWith] - String that ID must end with
100
101
  * @property {boolean} [caseSensitive=true] - Whether validation is case-sensitive
101
102
  * @property {Function} [transform] - Custom transformation function for ID
102
- * @property {string | null} [defaultValue] - Default value when input is empty
103
+ * @property {any} [defaultValue] - Default value when input is empty (string for string types, number for numeric)
103
104
  * @property {Record<Locale, IdMessages>} [i18n] - Custom error messages for different locales
104
105
  */
105
- export type IdOptions<IsRequired extends boolean = true> = {
106
- type?: IdType
106
+ export type IdOptions<Type extends IdType | undefined = undefined> = {
107
+ type?: Type
107
108
  minLength?: number
108
109
  maxLength?: number
109
110
  allowedTypes?: IdType[]
@@ -114,18 +115,27 @@ export type IdOptions<IsRequired extends boolean = true> = {
114
115
  endsWith?: string
115
116
  caseSensitive?: boolean
116
117
  transform?: (value: string) => string
117
- defaultValue?: IsRequired extends true ? string : string | null
118
+ defaultValue?: any // Simplified to avoid complex conditional types
118
119
  i18n?: Record<Locale, IdMessages>
119
120
  }
120
121
 
121
122
  /**
122
- * Type alias for ID validation schema based on required flag
123
+ * Type alias for ID validation schema based on required flag and ID type
123
124
  *
124
125
  * @template IsRequired - Whether the field is required
126
+ * @template IdType - The ID type being validated
125
127
  * @typedef IdSchema
126
- * @description Returns ZodString if required, ZodNullable<ZodString> if optional
128
+ * @description Returns appropriate Zod type based on required flag and ID type:
129
+ * - numeric type: ZodNumber or ZodNullable<ZodNumber>
130
+ * - other types: ZodString or ZodNullable<ZodString>
127
131
  */
128
- export type IdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
132
+ export type IdSchema<IsRequired extends boolean, Type extends IdType | undefined = undefined> = Type extends "numeric"
133
+ ? IsRequired extends true
134
+ ? ZodNumber
135
+ : ZodNullable<ZodNumber>
136
+ : IsRequired extends true
137
+ ? ZodString
138
+ : ZodNullable<ZodString>
129
139
 
130
140
  /**
131
141
  * Regular expression patterns for different ID formats
@@ -216,9 +226,9 @@ const validateIdType = (value: string, type: IdType): boolean => {
216
226
  * Creates a Zod schema for ID validation with comprehensive format support
217
227
  *
218
228
  * @template IsRequired - Whether the field is required (affects return type)
229
+ * @template Type - The ID type being validated (affects return type for numeric)
219
230
  * @param {IsRequired} [required=false] - Whether the field is required
220
- * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
221
- * @returns {IdSchema<IsRequired>} Zod schema for ID validation
231
+ * @returns {IdSchema<IsRequired, Type>} Zod schema for ID validation
222
232
  *
223
233
  * @description
224
234
  * Creates a comprehensive ID validator with support for multiple ID formats,
@@ -286,27 +296,31 @@ const validateIdType = (value: string, type: IdType): boolean => {
286
296
  * @see {@link detectIdType} for auto-detection logic
287
297
  * @see {@link validateIdType} for type-specific validation
288
298
  */
289
- export function id<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<IdOptions<IsRequired>, 'required'>): IdSchema<IsRequired> {
290
- const {
291
- type = "auto",
292
- minLength,
293
- maxLength,
294
- allowedTypes,
295
- customRegex,
296
- includes,
297
- excludes,
298
- startsWith,
299
- endsWith,
300
- caseSensitive = true,
301
- transform,
302
- defaultValue,
303
- i18n,
304
- } = options ?? {}
305
-
306
- const isRequired = required ?? false as IsRequired
307
-
308
- // Set appropriate default value based on required flag
309
- const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
299
+ // Overload: no options provided
300
+ export function id<IsRequired extends boolean = false>(required?: IsRequired): IdSchema<IsRequired, undefined>
301
+
302
+ // Overload: options with numeric type
303
+ export function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions<"numeric">, "required"> & { type: "numeric" }): IdSchema<IsRequired, "numeric">
304
+
305
+ // Overload: options with other specific type
306
+ export function id<IsRequired extends boolean = false, Type extends Exclude<IdType, "numeric"> = Exclude<IdType, "numeric">>(
307
+ required: IsRequired,
308
+ options: Omit<IdOptions<Type>, "required"> & { type: Type }
309
+ ): IdSchema<IsRequired, Type>
310
+
311
+ // Overload: options without type specified
312
+ export function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions, "required"> & { type?: never }): IdSchema<IsRequired, undefined>
313
+
314
+ // Implementation
315
+ export function id<IsRequired extends boolean = false, Type extends IdType | undefined = undefined>(required?: IsRequired, options?: any): any {
316
+ const { type = "auto" as Type, minLength, maxLength, allowedTypes, customRegex, includes, excludes, startsWith, endsWith, caseSensitive = true, transform, defaultValue, i18n } = options ?? {}
317
+
318
+ const isRequired = (required ?? false) as IsRequired
319
+ const isNumericType = type === "numeric"
320
+
321
+ // Set appropriate default value based on required flag and type
322
+ // For required fields, we don't set a default unless explicitly provided
323
+ const actualDefaultValue = defaultValue !== undefined ? defaultValue : isRequired ? (isNumericType ? NaN : "") : null
310
324
 
311
325
  // Helper function to get custom message or fallback to default i18n
312
326
  const getMessage = (key: keyof IdMessages, params?: Record<string, any>) => {
@@ -315,14 +329,30 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
315
329
  const customMessages = i18n[currentLocale]
316
330
  if (customMessages && customMessages[key]) {
317
331
  const template = customMessages[key]!
318
- return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
332
+ return template.replace(/\$\{(\w+)}/g, (_match: string, k: string) => params?.[k] ?? "")
319
333
  }
320
334
  }
321
335
  return t(`common.id.${key}`, params)
322
336
  }
323
337
 
324
- // Preprocessing function
325
- const preprocessFn = (val: unknown) => {
338
+ // Preprocessing function for numeric type
339
+ const preprocessNumericFn = (val: unknown) => {
340
+ // Handle empty/null values
341
+ if (val === "" || val === null || val === undefined) {
342
+ // If required and no default, return a special marker that will fail validation
343
+ if (isRequired && defaultValue === undefined) {
344
+ // Return undefined to trigger required error in refine
345
+ return undefined as any
346
+ }
347
+ return actualDefaultValue
348
+ }
349
+
350
+ // Try to convert to number and return (even if NaN) so it can be validated by the schema
351
+ return Number(val)
352
+ }
353
+
354
+ // Preprocessing function for string type
355
+ const preprocessStringFn = (val: unknown) => {
326
356
  if (val === "" || val === null || val === undefined) {
327
357
  return actualDefaultValue
328
358
  }
@@ -336,7 +366,44 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
336
366
  return processed
337
367
  }
338
368
 
339
- const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
369
+ // Create base schema based on type
370
+ if (isNumericType) {
371
+ // Use z.any() to avoid Zod's built-in type checking, then validate manually
372
+ const numericSchema = z.preprocess(preprocessNumericFn, z.any()).refine((val) => {
373
+ // Allow null for optional fields
374
+ if (!isRequired && val === null) return true
375
+
376
+ // Required check for undefined/null/empty (empty string when required)
377
+ if (val === undefined || (isRequired && val === null)) {
378
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
379
+ }
380
+
381
+ // Numeric validation - check if it's an actual number (not NaN)
382
+ if (typeof val !== "number" || isNaN(val)) {
383
+ throw new z.ZodError([{ code: "custom", message: getMessage("numeric"), path: [] }])
384
+ }
385
+
386
+ // Length checks on string representation
387
+ const strVal = String(val)
388
+ if (!ID_PATTERNS.numeric.test(strVal)) {
389
+ throw new z.ZodError([{ code: "custom", message: getMessage("numeric"), path: [] }])
390
+ }
391
+
392
+ if (minLength !== undefined && strVal.length < minLength) {
393
+ throw new z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }])
394
+ }
395
+ if (maxLength !== undefined && strVal.length > maxLength) {
396
+ throw new z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }])
397
+ }
398
+
399
+ return true
400
+ })
401
+
402
+ return numericSchema as unknown as IdSchema<IsRequired, Type>
403
+ }
404
+
405
+ // String-based ID validation
406
+ const baseSchema = isRequired ? z.preprocess(preprocessStringFn, z.string()) : z.preprocess(preprocessStringFn, z.string().nullable())
340
407
 
341
408
  const schema = baseSchema
342
409
  .refine((val) => {
@@ -372,12 +439,12 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
372
439
 
373
440
  if (allowedTypes && allowedTypes.length > 0) {
374
441
  // Check if ID matches any of the allowed types
375
- isValidId = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
442
+ isValidId = allowedTypes.some((allowedType: IdType) => validateIdType(val, allowedType))
376
443
  if (!isValidId) {
377
444
  const typeNames = allowedTypes.join(", ")
378
445
  throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
379
446
  }
380
- } else if (type !== "auto") {
447
+ } else if (type && type !== "auto") {
381
448
  // Validate specific type
382
449
  isValidId = validateIdType(val, type)
383
450
  if (!isValidId) {
@@ -390,10 +457,10 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
390
457
  throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
391
458
  }
392
459
  }
393
- } else if (val !== null && hasContentValidations && type !== "auto" && !customRegex) {
460
+ } else if (val !== null && hasContentValidations && type && type !== "auto" && !customRegex) {
394
461
  // Still validate specific types even with content validations (but not auto)
395
462
  if (allowedTypes && allowedTypes.length > 0) {
396
- const isValidType = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
463
+ const isValidType = allowedTypes.some((allowedType: IdType) => validateIdType(val, allowedType))
397
464
  if (!isValidType) {
398
465
  const typeNames = allowedTypes.join(", ")
399
466
  throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
@@ -444,7 +511,7 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
444
511
  return val // preserve the original case for UUID/ObjectId or when case-sensitive
445
512
  })
446
513
 
447
- return schema as unknown as IdSchema<IsRequired>
514
+ return schema as unknown as IdSchema<IsRequired, Type>
448
515
  }
449
516
 
450
517
  /**
@@ -135,8 +135,8 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
135
135
  })
136
136
 
137
137
  it("should apply transform function", () => {
138
- const schema = id(true, { type: "numeric", transform: (val) => val.toUpperCase() })
139
- expect(schema.parse("123")).toBe("123")
138
+ const schema = id(true, { type: "uuid", transform: (val) => val.toUpperCase() })
139
+ expect(schema.parse("550e8400-e29b-41d4-a716-446655440000")).toBe("550E8400-E29B-41D4-A716-446655440000")
140
140
  })
141
141
  })
142
142
 
@@ -162,7 +162,9 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
162
162
  it("should accept valid numeric IDs", () => {
163
163
  const schema = id(true, { type: "numeric" })
164
164
  validIds.numeric.forEach((validId) => {
165
- expect(schema.parse(validId)).toBe(validId)
165
+ const result = schema.parse(validId)
166
+ expect(typeof result).toBe("number")
167
+ expect(result).toBe(Number(validId))
166
168
  })
167
169
  })
168
170
 
@@ -461,7 +463,8 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
461
463
  expect(schema.parse("")).toBe(null)
462
464
  expect(() => schema.parse("ab")).toThrow() // not numeric
463
465
  expect(() => schema.parse("12")).toThrow() // too short
464
- expect(schema.parse("12345")).toBe("12345")
466
+ expect(schema.parse("12345")).toBe(12345) // Returns number for numeric type
467
+ expect(typeof schema.parse("12345")).toBe("number")
465
468
  })
466
469
 
467
470
  it("should handle multiple allowed types with constraints", () => {
@@ -499,4 +502,59 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
499
502
  expect(ID_PATTERNS.objectId.test("507f1f77bcf86cd799439011")).toBe(true)
500
503
  })
501
504
  })
505
+
506
+ describe("numeric type returns number", () => {
507
+ it("should return number type when type is numeric and required is true", () => {
508
+ const schema = id(true, { type: "numeric" })
509
+ const result = schema.parse("123")
510
+ expect(result).toBe(123)
511
+ expect(typeof result).toBe("number")
512
+ })
513
+
514
+ it("should return number | null when type is numeric and required is false", () => {
515
+ const schema = id(false, { type: "numeric" })
516
+ const result = schema.parse("456")
517
+ expect(result).toBe(456)
518
+ expect(typeof result).toBe("number")
519
+
520
+ const nullResult = schema.parse(null)
521
+ expect(nullResult).toBe(null)
522
+
523
+ const emptyResult = schema.parse("")
524
+ expect(emptyResult).toBe(null)
525
+ })
526
+
527
+ it("should use numeric default value for numeric type", () => {
528
+ const schema = id(true, { type: "numeric", defaultValue: 999 })
529
+ const result = schema.parse("")
530
+ expect(result).toBe(999)
531
+ expect(typeof result).toBe("number")
532
+ })
533
+
534
+ it("should validate numeric ID constraints", () => {
535
+ const schema = id(true, { type: "numeric", minLength: 3, maxLength: 5 })
536
+
537
+ expect(schema.parse("123")).toBe(123)
538
+ expect(schema.parse("12345")).toBe(12345)
539
+
540
+ // Too short
541
+ expect(() => schema.parse("12")).toThrow()
542
+
543
+ // Too long
544
+ expect(() => schema.parse("123456")).toThrow()
545
+
546
+ // Not numeric
547
+ expect(() => schema.parse("abc")).toThrow()
548
+ })
549
+
550
+ it("should convert string numbers to number type", () => {
551
+ const schema = id(true, { type: "numeric" })
552
+
553
+ expect(schema.parse("0")).toBe(0)
554
+ expect(schema.parse("999999")).toBe(999999)
555
+ expect(schema.parse(123)).toBe(123)
556
+
557
+ expect(typeof schema.parse("123")).toBe("number")
558
+ })
559
+ })
502
560
  })