@hy_ong/zod-kit 0.0.6 → 0.1.1

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.
Files changed (43) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/README.md +134 -68
  3. package/dist/index.cjs +93 -89
  4. package/dist/index.d.cts +235 -169
  5. package/dist/index.d.ts +235 -169
  6. package/dist/index.js +93 -89
  7. package/package.json +15 -5
  8. package/src/validators/common/boolean.ts +17 -14
  9. package/src/validators/common/date.ts +21 -14
  10. package/src/validators/common/datetime.ts +21 -14
  11. package/src/validators/common/email.ts +18 -15
  12. package/src/validators/common/file.ts +20 -13
  13. package/src/validators/common/id.ts +14 -14
  14. package/src/validators/common/number.ts +18 -15
  15. package/src/validators/common/password.ts +21 -14
  16. package/src/validators/common/text.ts +21 -17
  17. package/src/validators/common/time.ts +21 -14
  18. package/src/validators/common/url.ts +22 -15
  19. package/src/validators/taiwan/business-id.ts +18 -11
  20. package/src/validators/taiwan/fax.ts +23 -14
  21. package/src/validators/taiwan/mobile.ts +23 -14
  22. package/src/validators/taiwan/national-id.ts +11 -12
  23. package/src/validators/taiwan/postal-code.ts +16 -17
  24. package/src/validators/taiwan/tel.ts +23 -14
  25. package/tests/common/boolean.test.ts +38 -38
  26. package/tests/common/date.test.ts +65 -65
  27. package/tests/common/datetime.test.ts +100 -118
  28. package/tests/common/email.test.ts +24 -28
  29. package/tests/common/file.test.ts +47 -51
  30. package/tests/common/id.test.ts +80 -113
  31. package/tests/common/number.test.ts +24 -25
  32. package/tests/common/password.test.ts +28 -35
  33. package/tests/common/text.test.ts +36 -37
  34. package/tests/common/time.test.ts +64 -82
  35. package/tests/common/url.test.ts +67 -67
  36. package/tests/taiwan/business-id.test.ts +22 -22
  37. package/tests/taiwan/fax.test.ts +33 -42
  38. package/tests/taiwan/mobile.test.ts +32 -41
  39. package/tests/taiwan/national-id.test.ts +31 -31
  40. package/tests/taiwan/postal-code.test.ts +142 -96
  41. package/tests/taiwan/tel.test.ts +33 -42
  42. package/debug.js +0 -21
  43. package/debug.ts +0 -16
@@ -69,7 +69,6 @@ export type NumberMessages = {
69
69
  * @property {Record<Locale, NumberMessages>} [i18n] - Custom error messages for different locales
70
70
  */
71
71
  export type NumberOptions<IsRequired extends boolean = true> = {
72
- required?: IsRequired
73
72
  min?: number
74
73
  max?: number
75
74
  defaultValue?: IsRequired extends true ? number : number | null
@@ -119,53 +118,55 @@ export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ?
119
118
  *
120
119
  * @example
121
120
  * ```typescript
122
- * // Basic number validation
121
+ * // Basic number validation (optional by default)
123
122
  * const basicSchema = number()
124
123
  * basicSchema.parse(42) // ✓ Valid
125
124
  * basicSchema.parse("42") // ✓ Valid (converted to number)
125
+ * basicSchema.parse(null) // ✓ Valid (optional)
126
+ *
127
+ * // Required number
128
+ * const requiredSchema = number(true)
129
+ * requiredSchema.parse(42) // ✓ Valid
130
+ * requiredSchema.parse(null) // ✗ Invalid (required)
126
131
  *
127
132
  * // Integer only
128
- * const integerSchema = number({ type: "integer" })
133
+ * const integerSchema = number(false, { type: "integer" })
129
134
  * integerSchema.parse(42) // ✓ Valid
130
135
  * integerSchema.parse(42.5) // ✗ Invalid
131
136
  *
132
137
  * // Range validation
133
- * const rangeSchema = number({ min: 0, max: 100 })
138
+ * const rangeSchema = number(true, { min: 0, max: 100 })
134
139
  * rangeSchema.parse(50) // ✓ Valid
135
140
  * rangeSchema.parse(150) // ✗ Invalid
136
141
  *
137
142
  * // Positive numbers only
138
- * const positiveSchema = number({ positive: true })
143
+ * const positiveSchema = number(true, { positive: true })
139
144
  * positiveSchema.parse(5) // ✓ Valid
140
145
  * positiveSchema.parse(-5) // ✗ Invalid
141
146
  *
142
147
  * // Multiple of constraint
143
- * const multipleSchema = number({ multipleOf: 5 })
148
+ * const multipleSchema = number(true, { multipleOf: 5 })
144
149
  * multipleSchema.parse(10) // ✓ Valid
145
150
  * multipleSchema.parse(7) // ✗ Invalid
146
151
  *
147
152
  * // Precision control
148
- * const precisionSchema = number({ precision: 2 })
153
+ * const precisionSchema = number(true, { precision: 2 })
149
154
  * precisionSchema.parse(3.14) // ✓ Valid
150
155
  * precisionSchema.parse(3.14159) // ✗ Invalid
151
156
  *
152
157
  * // Comma-separated parsing
153
- * const commaSchema = number({ parseCommas: true })
158
+ * const commaSchema = number(false, { parseCommas: true })
154
159
  * commaSchema.parse("1,234.56") // ✓ Valid (parsed as 1234.56)
155
160
  *
156
161
  * // Optional with default
157
- * const optionalSchema = number({
158
- * required: false,
159
- * defaultValue: 0
160
- * })
162
+ * const optionalSchema = number(false, { defaultValue: 0 })
161
163
  * ```
162
164
  *
163
165
  * @throws {z.ZodError} When validation fails with specific error messages
164
166
  * @see {@link NumberOptions} for all available configuration options
165
167
  */
166
- export function number<IsRequired extends boolean = true>(options?: NumberOptions<IsRequired>): NumberSchema<IsRequired> {
168
+ export function number<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<NumberOptions<IsRequired>, 'required'>): NumberSchema<IsRequired> {
167
169
  const {
168
- required = true,
169
170
  min,
170
171
  max,
171
172
  defaultValue,
@@ -182,6 +183,8 @@ export function number<IsRequired extends boolean = true>(options?: NumberOption
182
183
  i18n,
183
184
  } = options ?? {}
184
185
 
186
+ const isRequired = required ?? false as IsRequired
187
+
185
188
  // Helper function to get custom message or fallback to default i18n
186
189
  const getMessage = (key: keyof NumberMessages, params?: Record<string, any>) => {
187
190
  if (i18n) {
@@ -242,7 +245,7 @@ export function number<IsRequired extends boolean = true>(options?: NumberOption
242
245
  )
243
246
  .refine((val) => {
244
247
  // Required check first
245
- if (required && val === null) {
248
+ if (isRequired && val === null) {
246
249
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
247
250
  }
248
251
 
@@ -85,7 +85,6 @@ export type PasswordStrength = "weak" | "medium" | "strong" | "very-strong"
85
85
  * @property {Record<Locale, PasswordMessages>} [i18n] - Custom error messages for different locales
86
86
  */
87
87
  export type PasswordOptions<IsRequired extends boolean = true> = {
88
- required?: IsRequired
89
88
  min?: number
90
89
  max?: number
91
90
  uppercase?: boolean
@@ -191,7 +190,8 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
191
190
  * Creates a Zod schema for password validation with comprehensive security checks
192
191
  *
193
192
  * @template IsRequired - Whether the field is required (affects return type)
194
- * @param {PasswordOptions<IsRequired>} [options] - Configuration options for password validation
193
+ * @param {IsRequired} [required=false] - Whether the field is required
194
+ * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
195
195
  * @returns {PasswordSchema<IsRequired>} Zod schema for password validation
196
196
  *
197
197
  * @description
@@ -212,11 +212,18 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
212
212
  * @example
213
213
  * ```typescript
214
214
  * // Basic password validation
215
- * const basicSchema = password()
215
+ * const basicSchema = password() // optional by default
216
216
  * basicSchema.parse("MyPassword123!") // ✓ Valid
217
+ * basicSchema.parse(null) // ✓ Valid (optional)
218
+ *
219
+ * // Required validation
220
+ * const requiredSchema = parse("MyPassword123!") // ✓ Valid
221
+ (true)
222
+ * requiredSchema.parse(null) // ✗ Invalid (required)
223
+ *
217
224
  *
218
225
  * // Strong password requirements
219
- * const strongSchema = password({
226
+ * const strongSchema = password(false, {
220
227
  * min: 12,
221
228
  * uppercase: true,
222
229
  * lowercase: true,
@@ -226,7 +233,7 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
226
233
  * })
227
234
  *
228
235
  * // No common passwords
229
- * const secureSchema = password({
236
+ * const secureSchema = password(false, {
230
237
  * noCommonWords: true,
231
238
  * noRepeating: true,
232
239
  * noSequential: true
@@ -236,7 +243,7 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
236
243
  * secureSchema.parse("abc123") // ✗ Invalid (sequential characters)
237
244
  *
238
245
  * // Custom requirements
239
- * const customSchema = password({
246
+ * const customSchema = password(false, {
240
247
  * min: 8,
241
248
  * includes: "@", // Must contain @
242
249
  * excludes: ["admin", "user"], // Cannot contain these words
@@ -244,13 +251,12 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
244
251
  * })
245
252
  *
246
253
  * // Minimum strength requirement
247
- * const strengthSchema = password({ minStrength: "very-strong" })
254
+ * const strengthSchema = password(false, { minStrength: "very-strong" })
248
255
  * strengthSchema.parse("weak") // ✗ Invalid (insufficient strength)
249
256
  * strengthSchema.parse("MyVeryStr0ng!P@ssw0rd2024") // ✓ Valid
250
257
  *
251
258
  * // Optional with default
252
- * const optionalSchema = password({
253
- * required: false,
259
+ * const optionalSchema = password(false, {
254
260
  * defaultValue: null
255
261
  * })
256
262
  * ```
@@ -260,9 +266,8 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
260
266
  * @see {@link PasswordStrength} for strength level definitions
261
267
  * @see {@link calculatePasswordStrength} for strength calculation logic
262
268
  */
263
- export function password<IsRequired extends boolean = true>(options?: PasswordOptions<IsRequired>): PasswordSchema<IsRequired> {
269
+ export function password<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<PasswordOptions<IsRequired>, 'required'>): PasswordSchema<IsRequired> {
264
270
  const {
265
- required = true,
266
271
  min,
267
272
  max,
268
273
  uppercase,
@@ -281,8 +286,10 @@ export function password<IsRequired extends boolean = true>(options?: PasswordOp
281
286
  i18n,
282
287
  } = options ?? {}
283
288
 
289
+ const isRequired = required ?? false as IsRequired
290
+
284
291
  // Set appropriate default value based on required flag
285
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
292
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
286
293
 
287
294
  // Helper function to get custom message or fallback to default i18n
288
295
  const getMessage = (key: keyof PasswordMessages, params?: Record<string, any>) => {
@@ -311,13 +318,13 @@ export function password<IsRequired extends boolean = true>(options?: PasswordOp
311
318
  return processed
312
319
  }
313
320
 
314
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
321
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
315
322
 
316
323
  const schema = baseSchema.refine((val) => {
317
324
  if (val === null) return true
318
325
 
319
326
  // Required check
320
- if (required && (val === "" || val === "null" || val === "undefined")) {
327
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
321
328
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
322
329
  }
323
330
 
@@ -60,7 +60,6 @@ export type TextMessages = {
60
60
  * @property {Record<Locale, TextMessages>} [i18n] - Custom error messages for different locales
61
61
  */
62
62
  export type TextOptions<IsRequired extends boolean = true> = {
63
- required?: IsRequired
64
63
  minLength?: number
65
64
  maxLength?: number
66
65
  startsWith?: string
@@ -108,17 +107,23 @@ export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? Z
108
107
  *
109
108
  * @example
110
109
  * ```typescript
111
- * // Basic text validation
110
+ * // Basic text validation (optional by default)
112
111
  * const basicSchema = text()
113
112
  * basicSchema.parse("Hello World") // ✓ Valid
113
+ * basicSchema.parse(null) // ✓ Valid (optional)
114
+ *
115
+ * // Required text
116
+ * const requiredSchema = text(true)
117
+ * requiredSchema.parse("Hello") // ✓ Valid
118
+ * requiredSchema.parse(null) // ✗ Invalid (required)
114
119
  *
115
120
  * // Length constraints
116
- * const lengthSchema = text({ minLength: 3, maxLength: 50 })
121
+ * const lengthSchema = text(true, { minLength: 3, maxLength: 50 })
117
122
  * lengthSchema.parse("Hello") // ✓ Valid
118
123
  * lengthSchema.parse("Hi") // ✗ Invalid (too short)
119
124
  *
120
125
  * // Content validation
121
- * const contentSchema = text({
126
+ * const contentSchema = text(true, {
122
127
  * startsWith: "Hello",
123
128
  * endsWith: "!",
124
129
  * includes: "World"
@@ -126,38 +131,37 @@ export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? Z
126
131
  * contentSchema.parse("Hello World!") // ✓ Valid
127
132
  *
128
133
  * // Case transformation
129
- * const upperSchema = text({ casing: "upper" })
134
+ * const upperSchema = text(false, { casing: "upper" })
130
135
  * upperSchema.parse("hello") // ✓ Valid (converted to "HELLO")
131
136
  *
132
137
  * // Trim modes
133
- * const trimStartSchema = text({ trimMode: "trimStart" })
138
+ * const trimStartSchema = text(false, { trimMode: "trimStart" })
134
139
  * trimStartSchema.parse(" hello ") // ✓ Valid (result: "hello ")
135
140
  *
136
141
  * // Regex validation
137
- * const regexSchema = text({ regex: /^[a-zA-Z]+$/ })
142
+ * const regexSchema = text(true, { regex: /^[a-zA-Z]+$/ })
138
143
  * regexSchema.parse("hello") // ✓ Valid
139
144
  * regexSchema.parse("hello123") // ✗ Invalid
140
145
  *
141
146
  * // Not empty (rejects whitespace-only)
142
- * const notEmptySchema = text({ notEmpty: true })
147
+ * const notEmptySchema = text(true, { notEmpty: true })
143
148
  * notEmptySchema.parse("hello") // ✓ Valid
144
149
  * notEmptySchema.parse(" ") // ✗ Invalid
145
150
  *
146
151
  * // Optional with default
147
- * const optionalSchema = text({
148
- * required: false,
149
- * defaultValue: "default text"
150
- * })
152
+ * const optionalSchema = text(false, { defaultValue: "default text" })
151
153
  * ```
152
154
  *
153
155
  * @throws {z.ZodError} When validation fails with specific error messages
154
156
  * @see {@link TextOptions} for all available configuration options
155
157
  */
156
- export function text<IsRequired extends boolean = true>(options?: TextOptions<IsRequired>): TextSchema<IsRequired> {
157
- const { required = true, minLength, maxLength, startsWith, endsWith, includes, excludes, regex, trimMode = "trim", casing = "none", transform, notEmpty, defaultValue, i18n } = options ?? {}
158
+ export function text<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TextOptions<IsRequired>, 'required'>): TextSchema<IsRequired> {
159
+ const { minLength, maxLength, startsWith, endsWith, includes, excludes, regex, trimMode = "trim", casing = "none", transform, notEmpty, defaultValue, i18n } = options ?? {}
160
+
161
+ const isRequired = required ?? false as IsRequired
158
162
 
159
163
  // Set appropriate default value based on required flag
160
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
164
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
161
165
 
162
166
  // Helper function to get custom message or fallback to default i18n
163
167
  const getMessage = (key: keyof TextMessages, params?: Record<string, any>) => {
@@ -217,14 +221,14 @@ export function text<IsRequired extends boolean = true>(options?: TextOptions<Is
217
221
  return processed
218
222
  }
219
223
 
220
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
224
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
221
225
 
222
226
  // Single refine with all validations for better performance
223
227
  const schema = baseSchema.refine((val) => {
224
228
  if (val === null) return true
225
229
 
226
230
  // Required check
227
- if (required && (val === "" || val === "null" || val === "undefined")) {
231
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
228
232
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
229
233
  }
230
234
 
@@ -92,7 +92,6 @@ export type TimeFormat =
92
92
  * @property {Record<Locale, TimeMessages>} [i18n] - Custom error messages for different locales
93
93
  */
94
94
  export type TimeOptions<IsRequired extends boolean = true> = {
95
- required?: IsRequired
96
95
  format?: TimeFormat
97
96
  min?: string // Minimum time (e.g., "09:00")
98
97
  max?: string // Maximum time (e.g., "17:00")
@@ -284,7 +283,8 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
284
283
  * Creates a Zod schema for time validation with comprehensive options
285
284
  *
286
285
  * @template IsRequired - Whether the field is required (affects return type)
287
- * @param {TimeOptions<IsRequired>} [options] - Configuration options for time validation
286
+ * @param {IsRequired} [required=false] - Whether the field is required
287
+ * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
288
288
  * @returns {TimeSchema<IsRequired>} Zod schema for time validation
289
289
  *
290
290
  * @description
@@ -306,10 +306,17 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
306
306
  * // Basic time validation (24-hour format)
307
307
  * const basicSchema = time()
308
308
  * basicSchema.parse("14:30") // ✓ Valid
309
+ * basicSchema.parse(null) // ✓ Valid (optional)
310
+ *
311
+ * // Required validation
312
+ * const requiredSchema = parse("14:30") // ✓ Valid
313
+ (true)
314
+ * requiredSchema.parse(null) // ✗ Invalid (required)
315
+ *
309
316
  * basicSchema.parse("2:30 PM") // ✗ Invalid (wrong format)
310
317
  *
311
318
  * // 12-hour format with AM/PM
312
- * const ampmSchema = time({ format: "hh:mm A" })
319
+ * const ampmSchema = time(false, { format: "hh:mm A" })
313
320
  * ampmSchema.parse("02:30 PM") // ✓ Valid
314
321
  * ampmSchema.parse("14:30") // ✗ Invalid (wrong format)
315
322
  *
@@ -325,7 +332,7 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
325
332
  * businessHours.parse("09:05") // ✗ Invalid (not 15-minute step)
326
333
  *
327
334
  * // Time range validation
328
- * const timeRangeSchema = time({
335
+ * const timeRangeSchema = time(false, {
329
336
  * min: "09:00",
330
337
  * max: "17:00"
331
338
  * })
@@ -340,7 +347,7 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
340
347
  * specificHours.parse("11:30") // ✗ Invalid (hour not allowed)
341
348
  *
342
349
  * // Whitelist specific times
343
- * const whitelistSchema = time({
350
+ * const whitelistSchema = time(false, {
344
351
  * whitelist: ["09:00", "12:00", "17:00"],
345
352
  * whitelistOnly: true
346
353
  * })
@@ -348,8 +355,7 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
348
355
  * whitelistSchema.parse("13:00") // ✗ Invalid (not in whitelist)
349
356
  *
350
357
  * // Optional with default
351
- * const optionalSchema = time({
352
- * required: false,
358
+ * const optionalSchema = time(false, {
353
359
  * defaultValue: "09:00"
354
360
  * })
355
361
  * optionalSchema.parse("") // ✓ Valid (returns "09:00")
@@ -359,9 +365,8 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
359
365
  * @see {@link TimeOptions} for all available configuration options
360
366
  * @see {@link TimeFormat} for supported time formats
361
367
  */
362
- export function time<IsRequired extends boolean = true>(options?: TimeOptions<IsRequired>): TimeSchema<IsRequired> {
368
+ export function time<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TimeOptions<IsRequired>, 'required'>): TimeSchema<IsRequired> {
363
369
  const {
364
- required = true,
365
370
  format = "HH:mm",
366
371
  min,
367
372
  max,
@@ -382,8 +387,10 @@ export function time<IsRequired extends boolean = true>(options?: TimeOptions<Is
382
387
  i18n,
383
388
  } = options ?? {}
384
389
 
390
+ const isRequired = required ?? false as IsRequired
391
+
385
392
  // Set appropriate default value based on required flag
386
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
393
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
387
394
 
388
395
  // Helper function to get custom message or fallback to default i18n
389
396
  const getMessage = (key: keyof TimeMessages, params?: Record<string, any>) => {
@@ -429,7 +436,7 @@ export function time<IsRequired extends boolean = true>(options?: TimeOptions<Is
429
436
  return ""
430
437
  }
431
438
  // If the field is optional and empty string not in whitelist, return default value
432
- if (!required) {
439
+ if (!isRequired) {
433
440
  return actualDefaultValue
434
441
  }
435
442
  // If a field is required, return the default value (will be validated later)
@@ -456,18 +463,18 @@ export function time<IsRequired extends boolean = true>(options?: TimeOptions<Is
456
463
  return processed
457
464
  }
458
465
 
459
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
466
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
460
467
 
461
468
  const schema = baseSchema.refine((val) => {
462
469
  if (val === null) return true
463
470
 
464
471
  // Required check
465
- if (required && (val === "" || val === "null" || val === "undefined")) {
472
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
466
473
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
467
474
  }
468
475
 
469
476
  if (val === null) return true
470
- if (!required && val === "") return true
477
+ if (!isRequired && val === "") return true
471
478
 
472
479
  // Whitelist check
473
480
  if (whitelist && whitelist.length > 0) {
@@ -85,7 +85,6 @@ export type UrlMessages = {
85
85
  * @property {Record<Locale, UrlMessages>} [i18n] - Custom error messages for different locales
86
86
  */
87
87
  export type UrlOptions<IsRequired extends boolean = true> = {
88
- required?: IsRequired
89
88
  min?: number
90
89
  max?: number
91
90
  includes?: string
@@ -121,7 +120,8 @@ export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? Zo
121
120
  * Creates a Zod schema for URL validation with comprehensive constraints
122
121
  *
123
122
  * @template IsRequired - Whether the field is required (affects return type)
124
- * @param {UrlOptions<IsRequired>} [options] - Configuration options for URL validation
123
+ * @param {IsRequired} [required=false] - Whether the field is required
124
+ * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
125
125
  * @returns {UrlSchema<IsRequired>} Zod schema for URL validation
126
126
  *
127
127
  * @description
@@ -145,43 +145,49 @@ export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? Zo
145
145
  * @example
146
146
  * ```typescript
147
147
  * // Basic URL validation
148
- * const basicSchema = url()
148
+ * const basicSchema = url() // optional by default
149
149
  * basicSchema.parse("https://example.com") // ✓ Valid
150
+ * basicSchema.parse(null) // ✓ Valid (optional)
151
+ *
152
+ * // Required validation
153
+ * const requiredSchema = parse("https://example.com") // ✓ Valid
154
+ (true)
155
+ * requiredSchema.parse(null) // ✗ Invalid (required)
156
+ *
150
157
  *
151
158
  * // HTTPS only
152
- * const httpsSchema = url({ protocols: ["https"] })
159
+ * const httpsSchema = url(false, { protocols: ["https"] })
153
160
  * httpsSchema.parse("https://example.com") // ✓ Valid
154
161
  * httpsSchema.parse("http://example.com") // ✗ Invalid
155
162
  *
156
163
  * // Domain restriction
157
- * const domainSchema = url({
164
+ * const domainSchema = url(false, {
158
165
  * allowedDomains: ["company.com", "trusted.org"]
159
166
  * })
160
167
  * domainSchema.parse("https://app.company.com") // ✓ Valid (subdomain)
161
168
  * domainSchema.parse("https://example.com") // ✗ Invalid
162
169
  *
163
170
  * // Block localhost
164
- * const noLocalhostSchema = url({ blockLocalhost: true })
171
+ * const noLocalhostSchema = url(false, { blockLocalhost: true })
165
172
  * noLocalhostSchema.parse("https://example.com") // ✓ Valid
166
173
  * noLocalhostSchema.parse("http://localhost:3000") // ✗ Invalid
167
174
  *
168
175
  * // API endpoints with path requirements
169
- * const apiSchema = url({
176
+ * const apiSchema = url(false, {
170
177
  * pathStartsWith: "/api/",
171
178
  * mustHaveQuery: true
172
179
  * })
173
180
  * apiSchema.parse("https://api.com/api/users?page=1") // ✓ Valid
174
181
  *
175
182
  * // Port restrictions
176
- * const portSchema = url({
183
+ * const portSchema = url(false, {
177
184
  * allowedPorts: [80, 443, 8080]
178
185
  * })
179
186
  * portSchema.parse("https://example.com:443") // ✓ Valid
180
187
  * portSchema.parse("https://example.com:3000") // ✗ Invalid
181
188
  *
182
189
  * // Optional with default
183
- * const optionalSchema = url({
184
- * required: false,
190
+ * const optionalSchema = url(false, {
185
191
  * defaultValue: null
186
192
  * })
187
193
  * ```
@@ -189,9 +195,8 @@ export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? Zo
189
195
  * @throws {z.ZodError} When validation fails with specific error messages
190
196
  * @see {@link UrlOptions} for all available configuration options
191
197
  */
192
- export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRequired>): UrlSchema<IsRequired> {
198
+ export function url<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<UrlOptions<IsRequired>, 'required'>): UrlSchema<IsRequired> {
193
199
  const {
194
- required = true,
195
200
  min,
196
201
  max,
197
202
  includes,
@@ -214,7 +219,9 @@ export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRe
214
219
  i18n,
215
220
  } = options ?? {}
216
221
 
217
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
222
+ const isRequired = required ?? false as IsRequired
223
+
224
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
218
225
 
219
226
  // Helper function to get custom message or fallback to default i18n
220
227
  const getMessage = (key: keyof UrlMessages, params?: Record<string, any>) => {
@@ -244,13 +251,13 @@ export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRe
244
251
  return processed
245
252
  }
246
253
 
247
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
254
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
248
255
 
249
256
  const schema = baseSchema.refine((val) => {
250
257
  if (val === null) return true
251
258
 
252
259
  // Required check
253
- if (required && (val === "" || val === "null" || val === "undefined")) {
260
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
254
261
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
255
262
  }
256
263
 
@@ -36,7 +36,6 @@ export type BusinessIdMessages = {
36
36
  * @property {Record<Locale, BusinessIdMessages>} [i18n] - Custom error messages for different locales
37
37
  */
38
38
  export type BusinessIdOptions<IsRequired extends boolean = true> = {
39
- required?: IsRequired
40
39
  transform?: (value: string) => string
41
40
  defaultValue?: IsRequired extends true ? string : string | null
42
41
  i18n?: Record<Locale, BusinessIdMessages>
@@ -148,23 +147,30 @@ const validateTaiwanBusinessId = (value: string): boolean => {
148
147
  * @example
149
148
  * ```typescript
150
149
  * // Basic business ID validation
151
- * const basicSchema = businessId()
150
+ * const basicSchema = businessId() // optional by default
152
151
  * basicSchema.parse("12345675") // ✓ Valid (if checksum correct)
152
+ * basicSchema.parse(null) // ✓ Valid (optional)
153
+ *
154
+ * // Required validation
155
+ * const requiredSchema = parse("12345675") // ✓ Valid (if checksum correct)
156
+ (true)
157
+ * requiredSchema.parse(null) // ✗ Invalid (required)
158
+ *
153
159
  * basicSchema.parse("1234567") // ✗ Invalid (not 8 digits)
154
160
  *
155
161
  * // Optional business ID
156
- * const optionalSchema = businessId({ required: false })
162
+ * const optionalSchema = businessId(false)
157
163
  * optionalSchema.parse("") // ✓ Valid (returns null)
158
164
  * optionalSchema.parse("12345675") // ✓ Valid (if checksum correct)
159
165
  *
160
166
  * // With custom transformation
161
- * const transformSchema = businessId({
167
+ * const transformSchema = businessId(false, {
162
168
  * transform: (value) => value.replace(/[^0-9]/g, '') // Remove non-digits
163
169
  * })
164
170
  * transformSchema.parse("1234-5675") // ✓ Valid (if checksum correct after cleaning)
165
171
  *
166
172
  * // With custom error messages
167
- * const customSchema = businessId({
173
+ * const customSchema = businessId(false, {
168
174
  * i18n: {
169
175
  * en: { invalid: "Please enter a valid Taiwan Business ID" },
170
176
  * 'zh-TW': { invalid: "請輸入有效的統一編號" }
@@ -176,16 +182,17 @@ const validateTaiwanBusinessId = (value: string): boolean => {
176
182
  * @see {@link BusinessIdOptions} for all available configuration options
177
183
  * @see {@link validateTaiwanBusinessId} for validation logic details
178
184
  */
179
- export function businessId<IsRequired extends boolean = true>(options?: BusinessIdOptions<IsRequired>): BusinessIdSchema<IsRequired> {
185
+ export function businessId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<BusinessIdOptions<IsRequired>, 'required'>): BusinessIdSchema<IsRequired> {
180
186
  const {
181
- required = true,
182
187
  transform,
183
188
  defaultValue,
184
189
  i18n
185
190
  } = options ?? {}
186
191
 
192
+ const isRequired = required ?? false as IsRequired
193
+
187
194
  // Set appropriate default value based on required flag
188
- const actualDefaultValue = defaultValue ?? (required ? "" : null)
195
+ const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
189
196
 
190
197
  // Helper function to get custom message or fallback to default i18n
191
198
  const getMessage = (key: keyof BusinessIdMessages, params?: Record<string, any>) => {
@@ -220,18 +227,18 @@ export function businessId<IsRequired extends boolean = true>(options?: Business
220
227
  return processed
221
228
  }
222
229
 
223
- const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
230
+ const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
224
231
 
225
232
  const schema = baseSchema.refine((val) => {
226
233
  if (val === null) return true
227
234
 
228
235
  // Required check
229
- if (required && (val === "" || val === "null" || val === "undefined")) {
236
+ if (isRequired && (val === "" || val === "null" || val === "undefined")) {
230
237
  throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
231
238
  }
232
239
 
233
240
  if (val === null) return true
234
- if (!required && val === "") return true
241
+ if (!isRequired && val === "") return true
235
242
 
236
243
  // Taiwan Business ID format validation (8 digits + checksum)
237
244
  if (!validateTaiwanBusinessId(val)) {