@hy_ong/zod-kit 0.1.1 → 0.1.3

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.
@@ -16,7 +16,7 @@ import { getLocale, type Locale } from "../../config"
16
16
  /**
17
17
  * Type definition for postal code validation error messages
18
18
  *
19
- * @interface PostalCodeMessages
19
+ * @interface TwPostalCodeMessages
20
20
  * @property {string} [required] - Message when field is required but empty
21
21
  * @property {string} [invalid] - Message when postal code format is invalid
22
22
  * @property {string} [invalidFormat] - Message when format doesn't match expected pattern
@@ -26,7 +26,7 @@ import { getLocale, type Locale } from "../../config"
26
26
  * @property {string} [format5Only] - Message when only 5-digit format is allowed
27
27
  * @property {string} [format6Only] - Message when only 6-digit format is allowed
28
28
  */
29
- export type PostalCodeMessages = {
29
+ export type TwPostalCodeMessages = {
30
30
  required?: string
31
31
  invalid?: string
32
32
  invalidFormat?: string
@@ -42,7 +42,7 @@ export type PostalCodeMessages = {
42
42
  /**
43
43
  * Postal code format types supported in Taiwan
44
44
  *
45
- * @typedef {"3" | "5" | "6" | "3+5" | "3+6" | "5+6" | "all"} PostalCodeFormat
45
+ * @typedef {"3" | "5" | "6" | "3+5" | "3+6" | "5+6" | "all"} TwPostalCodeFormatType
46
46
  *
47
47
  * Available formats:
48
48
  * - "3": 3-digit basic postal codes (100-982)
@@ -53,7 +53,7 @@ export type PostalCodeMessages = {
53
53
  * - "5+6": Accept both 5-digit and 6-digit formats
54
54
  * - "all": Accept all formats (3, 5, and 6 digits)
55
55
  */
56
- export type PostalCodeFormat =
56
+ export type TwPostalCodeFormatType =
57
57
  | "3" // 3-digit only
58
58
  | "5" // 5-digit only (legacy)
59
59
  | "6" // 6-digit only (current)
@@ -67,9 +67,9 @@ export type PostalCodeFormat =
67
67
  *
68
68
  * @template IsRequired - Whether the field is required (affects return type)
69
69
  *
70
- * @interface PostalCodeOptions
70
+ * @interface TwPostalCodeOptions
71
71
  * @property {IsRequired} [required=true] - Whether the field is required
72
- * @property {PostalCodeFormat} [format="3+6"] - Which postal code formats to accept
72
+ * @property {TwPostalCodeFormatType} [format="3+6"] - Which postal code formats to accept
73
73
  * @property {boolean} [strictValidation=true] - Enable strict validation against known postal code ranges
74
74
  * @property {boolean} [allowDashes=true] - Whether to allow dashes in postal codes (e.g., "100-01" or "100-001")
75
75
  * @property {boolean} [warn5Digit=true] - Whether to show warning for 5-digit legacy format
@@ -77,10 +77,10 @@ export type PostalCodeFormat =
77
77
  * @property {string[]} [blockedPrefixes] - Specific 3-digit prefixes to block
78
78
  * @property {Function} [transform] - Custom transformation function for postal codes
79
79
  * @property {string | null} [defaultValue] - Default value when input is empty
80
- * @property {Record<Locale, PostalCodeMessages>} [i18n] - Custom error messages for different locales
80
+ * @property {Record<Locale, TwPostalCodeMessages>} [i18n] - Custom error messages for different locales
81
81
  */
82
- export type PostalCodeOptions<IsRequired extends boolean = true> = {
83
- format?: PostalCodeFormat
82
+ export type TwPostalCodeOptions<IsRequired extends boolean = true> = {
83
+ format?: TwPostalCodeFormatType
84
84
  strictValidation?: boolean
85
85
  allowDashes?: boolean
86
86
  warn5Digit?: boolean
@@ -88,7 +88,7 @@ export type PostalCodeOptions<IsRequired extends boolean = true> = {
88
88
  blockedPrefixes?: string[]
89
89
  transform?: (value: string) => string
90
90
  defaultValue?: IsRequired extends true ? string : string | null
91
- i18n?: Record<Locale, PostalCodeMessages>
91
+ i18n?: Record<Locale, TwPostalCodeMessages>
92
92
  strictSuffixValidation?: boolean
93
93
  deprecate5Digit?: boolean
94
94
  }
@@ -97,10 +97,10 @@ export type PostalCodeOptions<IsRequired extends boolean = true> = {
97
97
  * Type alias for postal code validation schema based on required flag
98
98
  *
99
99
  * @template IsRequired - Whether the field is required
100
- * @typedef PostalCodeSchema
100
+ * @typedef TwPostalCodeSchema
101
101
  * @description Returns ZodString if required, ZodNullable<ZodString> if optional
102
102
  */
103
- export type PostalCodeSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
103
+ export type TwPostalCodeSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
104
104
 
105
105
  /**
106
106
  * Valid 3-digit postal code prefixes for Taiwan
@@ -728,7 +728,7 @@ const validate6DigitPostalCode = (value: string, strictValidation: boolean = tru
728
728
  * Main validation function for Taiwan postal codes
729
729
  *
730
730
  * @param {string} value - The postal code to validate
731
- * @param {PostalCodeFormat} format - Which formats to accept
731
+ * @param {TwPostalCodeFormatType} format - Which formats to accept
732
732
  * @param {boolean} strictValidation - Whether to validate against known postal codes
733
733
  * @param {boolean} strictSuffixValidation - Whether to validate suffix ranges
734
734
  * @param {boolean} allowDashes - Whether dashes/spaces are allowed and should be removed
@@ -738,7 +738,7 @@ const validate6DigitPostalCode = (value: string, strictValidation: boolean = tru
738
738
  */
739
739
  const validateTaiwanPostalCode = (
740
740
  value: string,
741
- format: PostalCodeFormat = "3+6",
741
+ format: TwPostalCodeFormatType = "3+6",
742
742
  strictValidation: boolean = true,
743
743
  strictSuffixValidation: boolean = false,
744
744
  allowDashes: boolean = true,
@@ -801,8 +801,8 @@ const validateTaiwanPostalCode = (
801
801
  * Creates a Zod schema for Taiwan postal code validation
802
802
  *
803
803
  * @template IsRequired - Whether the field is required (affects return type)
804
- * @param {PostalCodeOptions<IsRequired>} [options] - Configuration options for postal code validation
805
- * @returns {PostalCodeSchema<IsRequired>} Zod schema for postal code validation
804
+ * @param {TwPostalCodeOptions<IsRequired>} [options] - Configuration options for postal code validation
805
+ * @returns {TwPostalCodeSchema<IsRequired>} Zod schema for postal code validation
806
806
  *
807
807
  * @description
808
808
  * Creates a comprehensive Taiwan postal code validator that supports multiple formats
@@ -822,53 +822,53 @@ const validateTaiwanPostalCode = (
822
822
  * @example
823
823
  * ```typescript
824
824
  * // Accept 3-digit or 6-digit formats (recommended)
825
- * const modernSchema = postalCode()
825
+ * const modernSchema = twPostalCode()
826
826
  * modernSchema.parse("100") // ✓ Valid 3-digit
827
827
  * modernSchema.parse("100001") // ✓ Valid 6-digit
828
828
  * modernSchema.parse("10001") // ✗ Invalid (5-digit not allowed)
829
829
  *
830
830
  * // Accept all formats
831
- * const flexibleSchema = postalCode(false, { format: "all" })
831
+ * const flexibleSchema = twPostalCode(false, { format: "all" })
832
832
  * flexibleSchema.parse("100") // ✓ Valid
833
833
  * flexibleSchema.parse("10001") // ✓ Valid
834
834
  * flexibleSchema.parse("100001") // ✓ Valid
835
835
  *
836
836
  * // Only 6-digit format (current standard)
837
- * const modernOnlySchema = postalCode(false, { format: "6" })
837
+ * const modernOnlySchema = twPostalCode(false, { format: "6" })
838
838
  * modernOnlySchema.parse("100001") // ✓ Valid
839
839
  * modernOnlySchema.parse("100") // ✗ Invalid
840
840
  *
841
841
  * // With dashes allowed
842
- * const dashSchema = postalCode(false, { allowDashes: true })
842
+ * const dashSchema = twPostalCode(false, { allowDashes: true })
843
843
  * dashSchema.parse("100-001") // ✓ Valid (normalized to "100001")
844
844
  * dashSchema.parse("100-01") // ✓ Valid if 5-digit format allowed
845
845
  *
846
846
  * // Specific areas only
847
- * const taipeiSchema = postalCode(false, {
847
+ * const taipeiSchema = twPostalCode(false, {
848
848
  * allowedPrefixes: ["100", "103", "104", "105", "106"]
849
849
  * })
850
850
  * taipeiSchema.parse("100001") // ✓ Valid (Taipei area)
851
851
  * taipeiSchema.parse("200001") // ✗ Invalid (not in allowlist)
852
852
  *
853
853
  * // Block specific areas
854
- * const blockedSchema = postalCode(false, {
854
+ * const blockedSchema = twPostalCode(false, {
855
855
  * blockedPrefixes: ["999"] // Block test codes
856
856
  * })
857
857
  *
858
858
  * // With warning for legacy format
859
- * const warnSchema = postalCode(false, {
859
+ * const warnSchema = twPostalCode(false, {
860
860
  * format: "all",
861
861
  * warn5Digit: true
862
862
  * })
863
863
  * // Will validate but may show warning for 5-digit codes
864
864
  *
865
865
  * // Optional with custom transformation
866
- * const optionalSchema = postalCode(false, {
866
+ * const optionalSchema = twPostalCode(false, {
867
867
  * transform: (value) => value.replace(/\D/g, '') // Remove non-digits
868
868
  * })
869
869
  *
870
870
  * // Strict suffix validation for real postal codes
871
- * const strictSchema = postalCode(false, {
871
+ * const strictSchema = twPostalCode(false, {
872
872
  * format: "6",
873
873
  * strictSuffixValidation: true // Validates suffix range 001-999
874
874
  * })
@@ -876,7 +876,7 @@ const validateTaiwanPostalCode = (
876
876
  * strictSchema.parse("100000") // ✗ Invalid (suffix 000 not allowed)
877
877
  *
878
878
  * // Deprecate 5-digit codes entirely
879
- * const modern2024Schema = postalCode(false, {
879
+ * const modern2024Schema = twPostalCode(false, {
880
880
  * format: "all",
881
881
  * deprecate5Digit: true // Throws error for any 5-digit code
882
882
  * })
@@ -885,11 +885,11 @@ const validateTaiwanPostalCode = (
885
885
  * ```
886
886
  *
887
887
  * @throws {z.ZodError} When validation fails with specific error messages
888
- * @see {@link PostalCodeOptions} for all available configuration options
889
- * @see {@link PostalCodeFormat} for supported formats
888
+ * @see {@link TwPostalCodeOptions} for all available configuration options
889
+ * @see {@link TwPostalCodeFormatType} for supported formats
890
890
  * @see {@link validateTaiwanPostalCode} for validation logic details
891
891
  */
892
- export function postalCode<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<PostalCodeOptions<IsRequired>, 'required'>): PostalCodeSchema<IsRequired> {
892
+ export function twPostalCode<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwPostalCodeOptions<IsRequired>, 'required'>): TwPostalCodeSchema<IsRequired> {
893
893
  const {
894
894
  format = "3+6",
895
895
  strictValidation = true,
@@ -910,7 +910,7 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
910
910
  const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
911
911
 
912
912
  // Helper function to get custom message or fallback to default i18n
913
- const getMessage = (key: keyof PostalCodeMessages, params?: Record<string, any>) => {
913
+ const getMessage = (key: keyof TwPostalCodeMessages, params?: Record<string, any>) => {
914
914
  if (i18n) {
915
915
  const currentLocale = getLocale()
916
916
  const customMessages = i18n[currentLocale]
@@ -949,34 +949,39 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
949
949
 
950
950
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
951
951
 
952
- const schema = baseSchema.refine((val) => {
953
- if (val === null) return true
952
+ const schema = baseSchema.superRefine((val, ctx) => {
953
+ if (val === null) return
954
954
 
955
955
  // Required check
956
956
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
957
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
957
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
958
+ return
958
959
  }
959
960
 
960
- if (val === null) return true
961
- if (!isRequired && val === "") return true
961
+ if (val === null) return
962
+ if (!isRequired && val === "") return
962
963
 
963
964
  // Format-specific validation
964
965
  const cleanValue = val.replace(/[-\s]/g, "")
965
966
 
966
967
  // Check if format matches expected pattern
967
968
  if (format === "3" && cleanValue.length !== 3) {
968
- throw new z.ZodError([{ code: "custom", message: getMessage("format3Only"), path: [] }])
969
+ ctx.addIssue({ code: "custom", message: getMessage("format3Only") })
970
+ return
969
971
  }
970
972
  if (format === "5" && cleanValue.length !== 5) {
971
- throw new z.ZodError([{ code: "custom", message: getMessage("format5Only"), path: [] }])
973
+ ctx.addIssue({ code: "custom", message: getMessage("format5Only") })
974
+ return
972
975
  }
973
976
  if (format === "6" && cleanValue.length !== 6) {
974
- throw new z.ZodError([{ code: "custom", message: getMessage("format6Only"), path: [] }])
977
+ ctx.addIssue({ code: "custom", message: getMessage("format6Only") })
978
+ return
975
979
  }
976
980
 
977
981
  // Check for deprecated 5-digit format
978
982
  if (deprecate5Digit && cleanValue.length === 5) {
979
- throw new z.ZodError([{ code: "custom", message: getMessage("deprecated5Digit"), path: [] }])
983
+ ctx.addIssue({ code: "custom", message: getMessage("deprecated5Digit") })
984
+ return
980
985
  }
981
986
 
982
987
  // Pre-validate suffix for better error messages before main validation
@@ -987,7 +992,8 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
987
992
  const suffixNum = parseInt(suffix, 10)
988
993
  const ranges = getPostalCodeRanges(prefix)
989
994
  if (suffixNum < ranges.range5[0] || suffixNum > ranges.range5[1]) {
990
- throw new z.ZodError([{ code: "custom", message: getMessage("invalidSuffix"), path: [] }])
995
+ ctx.addIssue({ code: "custom", message: getMessage("invalidSuffix") })
996
+ return
991
997
  }
992
998
  } else if (cleanValue.length === 6) {
993
999
  const prefix = cleanValue.substring(0, 3)
@@ -995,25 +1001,25 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
995
1001
  const suffixNum = parseInt(suffix, 10)
996
1002
  const ranges = getPostalCodeRanges(prefix)
997
1003
  if (suffixNum < ranges.range6[0] || suffixNum > ranges.range6[1]) {
998
- throw new z.ZodError([{ code: "custom", message: getMessage("invalidSuffix"), path: [] }])
1004
+ ctx.addIssue({ code: "custom", message: getMessage("invalidSuffix") })
1005
+ return
999
1006
  }
1000
1007
  }
1001
1008
  }
1002
1009
 
1003
1010
  // Main postal code validation (only validates prefix if strictSuffixValidation already passed)
1004
1011
  if (!validateTaiwanPostalCode(val, format, strictValidation, strictSuffixValidation, allowDashes, allowedPrefixes, blockedPrefixes)) {
1005
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
1012
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
1013
+ return
1006
1014
  }
1007
1015
 
1008
1016
  // Warning for 5-digit legacy format (doesn't fail validation)
1009
1017
  if (warn5Digit && cleanValue.length === 5 && format !== "5" && !deprecate5Digit) {
1010
1018
  console.warn(getMessage("legacy5DigitWarning"))
1011
1019
  }
1012
-
1013
- return true
1014
1020
  })
1015
1021
 
1016
- return schema as unknown as PostalCodeSchema<IsRequired>
1022
+ return schema as unknown as TwPostalCodeSchema<IsRequired>
1017
1023
  }
1018
1024
 
1019
1025
  /**
@@ -15,12 +15,12 @@ import { getLocale, type Locale } from "../../config"
15
15
  /**
16
16
  * Type definition for telephone validation error messages
17
17
  *
18
- * @interface TelMessages
18
+ * @interface TwTelMessages
19
19
  * @property {string} [required] - Message when field is required but empty
20
20
  * @property {string} [invalid] - Message when telephone number format is invalid
21
21
  * @property {string} [notInWhitelist] - Message when telephone number is not in whitelist
22
22
  */
23
- export type TelMessages = {
23
+ export type TwTelMessages = {
24
24
  required?: string
25
25
  invalid?: string
26
26
  notInWhitelist?: string
@@ -31,28 +31,28 @@ export type TelMessages = {
31
31
  *
32
32
  * @template IsRequired - Whether the field is required (affects return type)
33
33
  *
34
- * @interface TelOptions
34
+ * @interface TwTelOptions
35
35
  * @property {IsRequired} [required=true] - Whether the field is required
36
36
  * @property {string[]} [whitelist] - Array of specific telephone numbers that are always allowed
37
37
  * @property {Function} [transform] - Custom transformation function for telephone number
38
38
  * @property {string | null} [defaultValue] - Default value when input is empty
39
- * @property {Record<Locale, TelMessages>} [i18n] - Custom error messages for different locales
39
+ * @property {Record<Locale, TwTelMessages>} [i18n] - Custom error messages for different locales
40
40
  */
41
- export type TelOptions<IsRequired extends boolean = true> = {
41
+ export type TwTelOptions<IsRequired extends boolean = true> = {
42
42
  whitelist?: string[]
43
43
  transform?: (value: string) => string
44
44
  defaultValue?: IsRequired extends true ? string : string | null
45
- i18n?: Record<Locale, TelMessages>
45
+ i18n?: Record<Locale, TwTelMessages>
46
46
  }
47
47
 
48
48
  /**
49
49
  * Type alias for telephone validation schema based on required flag
50
50
  *
51
51
  * @template IsRequired - Whether the field is required
52
- * @typedef TelSchema
52
+ * @typedef TwTelSchema
53
53
  * @description Returns ZodString if required, ZodNullable<ZodString> if optional
54
54
  */
55
- export type TelSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
55
+ export type TwTelSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
56
56
 
57
57
  /**
58
58
  * Validates Taiwan landline telephone number format (Official 2024 rules)
@@ -174,7 +174,7 @@ const validateTaiwanTel = (value: string): boolean => {
174
174
  * @template IsRequired - Whether the field is required (affects return type)
175
175
  * @param {IsRequired} [required=false] - Whether the field is required
176
176
  * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
177
- * @returns {TelSchema<IsRequired>} Zod schema for telephone number validation
177
+ * @returns {TwTelSchema<IsRequired>} Zod schema for telephone number validation
178
178
  *
179
179
  * @description
180
180
  * Creates a comprehensive Taiwan landline telephone number validator with support for
@@ -193,7 +193,7 @@ const validateTaiwanTel = (value: string): boolean => {
193
193
  * @example
194
194
  * ```typescript
195
195
  * // Basic telephone number validation
196
- * const basicSchema = tel() // optional by default
196
+ * const basicSchema = twTel() // optional by default
197
197
  * basicSchema.parse("0223456789") // ✓ Valid (Taipei)
198
198
  * basicSchema.parse(null) // ✓ Valid (optional)
199
199
  *
@@ -207,26 +207,26 @@ const validateTaiwanTel = (value: string): boolean => {
207
207
  * basicSchema.parse("0812345678") // ✗ Invalid (wrong format for 08)
208
208
  *
209
209
  * // With whitelist (only specific numbers allowed)
210
- * const whitelistSchema = tel(false, {
210
+ * const whitelistSchema = twTel(false, {
211
211
  * whitelist: ["0223456789", "0312345678"]
212
212
  * })
213
213
  * whitelistSchema.parse("0223456789") // ✓ Valid (in whitelist)
214
214
  * whitelistSchema.parse("0287654321") // ✗ Invalid (not in whitelist)
215
215
  *
216
216
  * // Optional telephone number
217
- * const optionalSchema = tel(false)
217
+ * const optionalSchema = twTel(false)
218
218
  * optionalSchema.parse("") // ✓ Valid (returns null)
219
219
  * optionalSchema.parse("0223456789") // ✓ Valid
220
220
  *
221
221
  * // With custom transformation (remove separators)
222
- * const transformSchema = tel(false, {
222
+ * const transformSchema = twTel(false, {
223
223
  * transform: (value) => value.replace(/[^0-9]/g, '') // Keep only digits
224
224
  * })
225
225
  * transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
226
226
  * transformSchema.parse("02 2345 6789") // ✓ Valid (spaces removed)
227
227
  *
228
228
  * // With custom error messages
229
- * const customSchema = tel(false, {
229
+ * const customSchema = twTel(false, {
230
230
  * i18n: {
231
231
  * en: { invalid: "Please enter a valid Taiwan landline number" },
232
232
  * 'zh-TW': { invalid: "請輸入有效的台灣市話號碼" }
@@ -235,10 +235,10 @@ const validateTaiwanTel = (value: string): boolean => {
235
235
  * ```
236
236
  *
237
237
  * @throws {z.ZodError} When validation fails with specific error messages
238
- * @see {@link TelOptions} for all available configuration options
238
+ * @see {@link TwTelOptions} for all available configuration options
239
239
  * @see {@link validateTaiwanTel} for validation logic details
240
240
  */
241
- export function tel<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TelOptions<IsRequired>, 'required'>): TelSchema<IsRequired> {
241
+ export function twTel<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwTelOptions<IsRequired>, 'required'>): TwTelSchema<IsRequired> {
242
242
  const { whitelist, transform, defaultValue, i18n } = options ?? {}
243
243
 
244
244
  const isRequired = required ?? false as IsRequired
@@ -247,7 +247,7 @@ export function tel<IsRequired extends boolean = false>(required?: IsRequired, o
247
247
  const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
248
248
 
249
249
  // Helper function to get custom message or fallback to default i18n
250
- const getMessage = (key: keyof TelMessages, params?: Record<string, any>) => {
250
+ const getMessage = (key: keyof TwTelMessages, params?: Record<string, any>) => {
251
251
  if (i18n) {
252
252
  const currentLocale = getLocale()
253
253
  const customMessages = i18n[currentLocale]
@@ -290,35 +290,36 @@ export function tel<IsRequired extends boolean = false>(required?: IsRequired, o
290
290
 
291
291
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
292
292
 
293
- const schema = baseSchema.refine((val) => {
294
- if (val === null) return true
293
+ const schema = baseSchema.superRefine((val, ctx) => {
294
+ if (val === null) return
295
295
 
296
296
  // Required check
297
297
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
298
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
298
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
299
+ return
299
300
  }
300
301
 
301
- if (val === null) return true
302
- if (!isRequired && val === "") return true
302
+ if (val === null) return
303
+ if (!isRequired && val === "") return
303
304
 
304
305
  // Allowlist check (if an allowlist is provided, only allow values in the allowlist)
305
306
  if (whitelist && whitelist.length > 0) {
306
307
  if (whitelist.includes(val)) {
307
- return true
308
+ return
308
309
  }
309
310
  // If not in the allowlist, reject regardless of format
310
- throw new z.ZodError([{ code: "custom", message: getMessage("notInWhitelist"), path: [] }])
311
+ ctx.addIssue({ code: "custom", message: getMessage("notInWhitelist") })
312
+ return
311
313
  }
312
314
 
313
315
  // Taiwan telephone format validation (only if no allowlist or allowlist is empty)
314
316
  if (!validateTaiwanTel(val)) {
315
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
317
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
318
+ return
316
319
  }
317
-
318
- return true
319
320
  })
320
321
 
321
- return schema as unknown as TelSchema<IsRequired>
322
+ return schema as unknown as TwTelSchema<IsRequired>
322
323
  }
323
324
 
324
325
  /**
@@ -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
  })