@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.
@@ -15,12 +15,12 @@ import { getLocale, type Locale } from "../../config"
15
15
  /**
16
16
  * Type definition for fax number validation error messages
17
17
  *
18
- * @interface FaxMessages
18
+ * @interface TwFaxMessages
19
19
  * @property {string} [required] - Message when field is required but empty
20
20
  * @property {string} [invalid] - Message when fax number format is invalid
21
21
  * @property {string} [notInWhitelist] - Message when fax number is not in whitelist
22
22
  */
23
- export type FaxMessages = {
23
+ export type TwFaxMessages = {
24
24
  required?: string
25
25
  invalid?: string
26
26
  notInWhitelist?: string
@@ -31,28 +31,28 @@ export type FaxMessages = {
31
31
  *
32
32
  * @template IsRequired - Whether the field is required (affects return type)
33
33
  *
34
- * @interface FaxOptions
34
+ * @interface TwFaxOptions
35
35
  * @property {IsRequired} [required=true] - Whether the field is required
36
36
  * @property {string[]} [whitelist] - Array of specific fax numbers that are always allowed
37
37
  * @property {Function} [transform] - Custom transformation function for fax number
38
38
  * @property {string | null} [defaultValue] - Default value when input is empty
39
- * @property {Record<Locale, FaxMessages>} [i18n] - Custom error messages for different locales
39
+ * @property {Record<Locale, TwFaxMessages>} [i18n] - Custom error messages for different locales
40
40
  */
41
- export type FaxOptions<IsRequired extends boolean = true> = {
41
+ export type TwFaxOptions<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, FaxMessages>
45
+ i18n?: Record<Locale, TwFaxMessages>
46
46
  }
47
47
 
48
48
  /**
49
49
  * Type alias for fax number validation schema based on required flag
50
50
  *
51
51
  * @template IsRequired - Whether the field is required
52
- * @typedef FaxSchema
52
+ * @typedef TwFaxSchema
53
53
  * @description Returns ZodString if required, ZodNullable<ZodString> if optional
54
54
  */
55
- export type FaxSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
55
+ export type TwFaxSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
56
56
 
57
57
  /**
58
58
  * Validates Taiwan fax number format (Official 2024 rules - same as landline)
@@ -172,7 +172,7 @@ const validateTaiwanFax = (value: string): boolean => {
172
172
  * @template IsRequired - Whether the field is required (affects return type)
173
173
  * @param {IsRequired} [required=false] - Whether the field is required
174
174
  * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
175
- * @returns {FaxSchema<IsRequired>} Zod schema for fax number validation
175
+ * @returns {TwFaxSchema<IsRequired>} Zod schema for fax number validation
176
176
  *
177
177
  * @description
178
178
  * Creates a comprehensive Taiwan fax number validator with support for all Taiwan
@@ -191,7 +191,7 @@ const validateTaiwanFax = (value: string): boolean => {
191
191
  * @example
192
192
  * ```typescript
193
193
  * // Basic fax number validation
194
- * const basicSchema = fax() // optional by default
194
+ * const basicSchema = twFax() // optional by default
195
195
  * basicSchema.parse("0223456789") // ✓ Valid (Taipei)
196
196
  * basicSchema.parse(null) // ✓ Valid (optional)
197
197
  *
@@ -205,25 +205,25 @@ const validateTaiwanFax = (value: string): boolean => {
205
205
  * basicSchema.parse("0812345678") // ✗ Invalid (wrong format for 08)
206
206
  *
207
207
  * // With whitelist (only specific numbers allowed)
208
- * const whitelistSchema = fax(false, {
208
+ * const whitelistSchema = twFax(false, {
209
209
  * whitelist: ["0223456789", "0312345678"]
210
210
  * })
211
211
  * whitelistSchema.parse("0223456789") // ✓ Valid (in whitelist)
212
212
  * whitelistSchema.parse("0287654321") // ✗ Invalid (not in whitelist)
213
213
  *
214
214
  * // Optional fax number
215
- * const optionalSchema = fax(false)
215
+ * const optionalSchema = twFax(false)
216
216
  * optionalSchema.parse("") // ✓ Valid (returns null)
217
217
  * optionalSchema.parse("0223456789") // ✓ Valid
218
218
  *
219
219
  * // With custom transformation
220
- * const transformSchema = fax(false, {
220
+ * const transformSchema = twFax(false, {
221
221
  * transform: (value) => value.replace(/[^0-9]/g, '') // Keep only digits
222
222
  * })
223
223
  * transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
224
224
  *
225
225
  * // With custom error messages
226
- * const customSchema = fax(false, {
226
+ * const customSchema = twFax(false, {
227
227
  * i18n: {
228
228
  * en: { invalid: "Please enter a valid Taiwan fax number" },
229
229
  * 'zh-TW': { invalid: "請輸入有效的台灣傳真號碼" }
@@ -232,10 +232,10 @@ const validateTaiwanFax = (value: string): boolean => {
232
232
  * ```
233
233
  *
234
234
  * @throws {z.ZodError} When validation fails with specific error messages
235
- * @see {@link FaxOptions} for all available configuration options
235
+ * @see {@link TwFaxOptions} for all available configuration options
236
236
  * @see {@link validateTaiwanFax} for validation logic details
237
237
  */
238
- export function fax<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<FaxOptions<IsRequired>, 'required'>): FaxSchema<IsRequired> {
238
+ export function twFax<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwFaxOptions<IsRequired>, 'required'>): TwFaxSchema<IsRequired> {
239
239
  const { whitelist, transform, defaultValue, i18n } = options ?? {}
240
240
 
241
241
  const isRequired = required ?? false as IsRequired
@@ -244,7 +244,7 @@ export function fax<IsRequired extends boolean = false>(required?: IsRequired, o
244
244
  const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
245
245
 
246
246
  // Helper function to get custom message or fallback to default i18n
247
- const getMessage = (key: keyof FaxMessages, params?: Record<string, any>) => {
247
+ const getMessage = (key: keyof TwFaxMessages, params?: Record<string, any>) => {
248
248
  if (i18n) {
249
249
  const currentLocale = getLocale()
250
250
  const customMessages = i18n[currentLocale]
@@ -287,35 +287,36 @@ export function fax<IsRequired extends boolean = false>(required?: IsRequired, o
287
287
 
288
288
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
289
289
 
290
- const schema = baseSchema.refine((val) => {
291
- if (val === null) return true
290
+ const schema = baseSchema.superRefine((val, ctx) => {
291
+ if (val === null) return
292
292
 
293
293
  // Required check
294
294
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
295
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
295
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
296
+ return
296
297
  }
297
298
 
298
- if (val === null) return true
299
- if (!isRequired && val === "") return true
299
+ if (val === null) return
300
+ if (!isRequired && val === "") return
300
301
 
301
302
  // Allowlist check (if an allowlist is provided, only allow values in the allowlist)
302
303
  if (whitelist && whitelist.length > 0) {
303
304
  if (whitelist.includes(val)) {
304
- return true
305
+ return
305
306
  }
306
307
  // If not in the allowlist, reject regardless of format
307
- throw new z.ZodError([{ code: "custom", message: getMessage("notInWhitelist"), path: [] }])
308
+ ctx.addIssue({ code: "custom", message: getMessage("notInWhitelist") })
309
+ return
308
310
  }
309
311
 
310
312
  // Taiwan fax format validation (only if no allowlist or allowlist is empty)
311
313
  if (!validateTaiwanFax(val)) {
312
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
314
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
315
+ return
313
316
  }
314
-
315
- return true
316
317
  })
317
318
 
318
- return schema as unknown as FaxSchema<IsRequired>
319
+ return schema as unknown as TwFaxSchema<IsRequired>
319
320
  }
320
321
 
321
322
  /**
@@ -15,12 +15,12 @@ import { getLocale, type Locale } from "../../config"
15
15
  /**
16
16
  * Type definition for mobile phone validation error messages
17
17
  *
18
- * @interface MobileMessages
18
+ * @interface TwMobileMessages
19
19
  * @property {string} [required] - Message when field is required but empty
20
20
  * @property {string} [invalid] - Message when mobile number format is invalid
21
21
  * @property {string} [notInWhitelist] - Message when mobile number is not in whitelist
22
22
  */
23
- export type MobileMessages = {
23
+ export type TwMobileMessages = {
24
24
  required?: string
25
25
  invalid?: string
26
26
  notInWhitelist?: string
@@ -31,28 +31,28 @@ export type MobileMessages = {
31
31
  *
32
32
  * @template IsRequired - Whether the field is required (affects return type)
33
33
  *
34
- * @interface MobileOptions
34
+ * @interface TwMobileOptions
35
35
  * @property {IsRequired} [required=true] - Whether the field is required
36
36
  * @property {string[]} [whitelist] - Array of specific mobile numbers that are always allowed
37
37
  * @property {Function} [transform] - Custom transformation function for mobile number
38
38
  * @property {string | null} [defaultValue] - Default value when input is empty
39
- * @property {Record<Locale, MobileMessages>} [i18n] - Custom error messages for different locales
39
+ * @property {Record<Locale, TwMobileMessages>} [i18n] - Custom error messages for different locales
40
40
  */
41
- export type MobileOptions<IsRequired extends boolean = true> = {
41
+ export type TwMobileOptions<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, MobileMessages>
45
+ i18n?: Record<Locale, TwMobileMessages>
46
46
  }
47
47
 
48
48
  /**
49
49
  * Type alias for mobile phone validation schema based on required flag
50
50
  *
51
51
  * @template IsRequired - Whether the field is required
52
- * @typedef MobileSchema
52
+ * @typedef TwMobileSchema
53
53
  * @description Returns ZodString if required, ZodNullable<ZodString> if optional
54
54
  */
55
- export type MobileSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
55
+ export type TwMobileSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
56
56
 
57
57
  /**
58
58
  * Validates Taiwan mobile phone number format
@@ -88,7 +88,7 @@ const validateTaiwanMobile = (value: string): boolean => {
88
88
  * @template IsRequired - Whether the field is required (affects return type)
89
89
  * @param {IsRequired} [required=false] - Whether the field is required
90
90
  * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
91
- * @returns {MobileSchema<IsRequired>} Zod schema for mobile phone validation
91
+ * @returns {TwMobileSchema<IsRequired>} Zod schema for mobile phone validation
92
92
  *
93
93
  * @description
94
94
  * Creates a comprehensive Taiwan mobile phone number validator with support for
@@ -106,7 +106,7 @@ const validateTaiwanMobile = (value: string): boolean => {
106
106
  * @example
107
107
  * ```typescript
108
108
  * // Basic mobile number validation
109
- * const basicSchema = mobile() // optional by default
109
+ * const basicSchema = twMobile() // optional by default
110
110
  * basicSchema.parse("0912345678") // ✓ Valid
111
111
  * basicSchema.parse(null) // ✓ Valid (optional)
112
112
  *
@@ -119,26 +119,26 @@ const validateTaiwanMobile = (value: string): boolean => {
119
119
  * basicSchema.parse("0812345678") // ✗ Invalid (wrong prefix)
120
120
  *
121
121
  * // With whitelist (only specific numbers allowed)
122
- * const whitelistSchema = mobile(false, {
122
+ * const whitelistSchema = twMobile(false, {
123
123
  * whitelist: ["0912345678", "0987654321"]
124
124
  * })
125
125
  * whitelistSchema.parse("0912345678") // ✓ Valid (in whitelist)
126
126
  * whitelistSchema.parse("0911111111") // ✗ Invalid (not in whitelist)
127
127
  *
128
128
  * // Optional mobile number
129
- * const optionalSchema = mobile(false)
129
+ * const optionalSchema = twMobile(false)
130
130
  * optionalSchema.parse("") // ✓ Valid (returns null)
131
131
  * optionalSchema.parse("0912345678") // ✓ Valid
132
132
  *
133
133
  * // With custom transformation
134
- * const transformSchema = mobile(false, {
134
+ * const transformSchema = twMobile(false, {
135
135
  * transform: (value) => value.replace(/[^0-9]/g, '') // Remove non-digits
136
136
  * })
137
137
  * transformSchema.parse("091-234-5678") // ✓ Valid (formatted input)
138
138
  * transformSchema.parse("091 234 5678") // ✓ Valid (spaced input)
139
139
  *
140
140
  * // With custom error messages
141
- * const customSchema = mobile(false, {
141
+ * const customSchema = twMobile(false, {
142
142
  * i18n: {
143
143
  * en: { invalid: "Please enter a valid Taiwan mobile number" },
144
144
  * 'zh-TW': { invalid: "請輸入有效的台灣手機號碼" }
@@ -147,10 +147,10 @@ const validateTaiwanMobile = (value: string): boolean => {
147
147
  * ```
148
148
  *
149
149
  * @throws {z.ZodError} When validation fails with specific error messages
150
- * @see {@link MobileOptions} for all available configuration options
150
+ * @see {@link TwMobileOptions} for all available configuration options
151
151
  * @see {@link validateTaiwanMobile} for validation logic details
152
152
  */
153
- export function mobile<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<MobileOptions<IsRequired>, 'required'>): MobileSchema<IsRequired> {
153
+ export function twMobile<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwMobileOptions<IsRequired>, 'required'>): TwMobileSchema<IsRequired> {
154
154
  const { whitelist, transform, defaultValue, i18n } = options ?? {}
155
155
 
156
156
  const isRequired = required ?? false as IsRequired
@@ -159,7 +159,7 @@ export function mobile<IsRequired extends boolean = false>(required?: IsRequired
159
159
  const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
160
160
 
161
161
  // Helper function to get custom message or fallback to default i18n
162
- const getMessage = (key: keyof MobileMessages, params?: Record<string, any>) => {
162
+ const getMessage = (key: keyof TwMobileMessages, params?: Record<string, any>) => {
163
163
  if (i18n) {
164
164
  const currentLocale = getLocale()
165
165
  const customMessages = i18n[currentLocale]
@@ -202,35 +202,36 @@ export function mobile<IsRequired extends boolean = false>(required?: IsRequired
202
202
 
203
203
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
204
204
 
205
- const schema = baseSchema.refine((val) => {
206
- if (val === null) return true
205
+ const schema = baseSchema.superRefine((val, ctx) => {
206
+ if (val === null) return
207
207
 
208
208
  // Required check
209
209
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
210
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
210
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
211
+ return
211
212
  }
212
213
 
213
- if (val === null) return true
214
- if (!isRequired && val === "") return true
214
+ if (val === null) return
215
+ if (!isRequired && val === "") return
215
216
 
216
217
  // Allowlist check (if an allowlist is provided, only allow values in the allowlist)
217
218
  if (whitelist && whitelist.length > 0) {
218
219
  if (whitelist.includes(val)) {
219
- return true
220
+ return
220
221
  }
221
222
  // If not in the allowlist, reject regardless of format
222
- throw new z.ZodError([{ code: "custom", message: getMessage("notInWhitelist"), path: [] }])
223
+ ctx.addIssue({ code: "custom", message: getMessage("notInWhitelist") })
224
+ return
223
225
  }
224
226
 
225
227
  // Taiwan mobile phone format validation (only if no allowlist or allowlist is empty)
226
228
  if (!validateTaiwanMobile(val)) {
227
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
229
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
230
+ return
228
231
  }
229
-
230
- return true
231
232
  })
232
233
 
233
- return schema as unknown as MobileSchema<IsRequired>
234
+ return schema as unknown as TwMobileSchema<IsRequired>
234
235
  }
235
236
 
236
237
  /**
@@ -15,11 +15,11 @@ import { getLocale, type Locale } from "../../config"
15
15
  /**
16
16
  * Type definition for national ID validation error messages
17
17
  *
18
- * @interface NationalIdMessages
18
+ * @interface TwNationalIdMessages
19
19
  * @property {string} [required] - Message when field is required but empty
20
20
  * @property {string} [invalid] - Message when national ID format or checksum is invalid
21
21
  */
22
- export type NationalIdMessages = {
22
+ export type TwNationalIdMessages = {
23
23
  required?: string
24
24
  invalid?: string
25
25
  }
@@ -44,30 +44,30 @@ export type NationalIdType =
44
44
  *
45
45
  * @template IsRequired - Whether the field is required (affects return type)
46
46
  *
47
- * @interface NationalIdOptions
47
+ * @interface TwNationalIdOptions
48
48
  * @property {IsRequired} [required=true] - Whether the field is required
49
49
  * @property {NationalIdType} [type="both"] - Type of ID to accept
50
50
  * @property {boolean} [allowOldResident=true] - Whether to accept old-style resident certificates
51
51
  * @property {Function} [transform] - Custom transformation function for ID
52
52
  * @property {string | null} [defaultValue] - Default value when input is empty
53
- * @property {Record<Locale, NationalIdMessages>} [i18n] - Custom error messages for different locales
53
+ * @property {Record<Locale, TwNationalIdMessages>} [i18n] - Custom error messages for different locales
54
54
  */
55
- export type NationalIdOptions<IsRequired extends boolean = true> = {
55
+ export type TwNationalIdOptions<IsRequired extends boolean = true> = {
56
56
  type?: NationalIdType
57
57
  allowOldResident?: boolean
58
58
  transform?: (value: string) => string
59
59
  defaultValue?: IsRequired extends true ? string : string | null
60
- i18n?: Record<Locale, NationalIdMessages>
60
+ i18n?: Record<Locale, TwNationalIdMessages>
61
61
  }
62
62
 
63
63
  /**
64
64
  * Type alias for national ID validation schema based on required flag
65
65
  *
66
66
  * @template IsRequired - Whether the field is required
67
- * @typedef NationalIdSchema
67
+ * @typedef TwNationalIdSchema
68
68
  * @description Returns ZodString if required, ZodNullable<ZodString> if optional
69
69
  */
70
- export type NationalIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
70
+ export type TwNationalIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
71
71
 
72
72
  /**
73
73
  * Mapping of Taiwan city/county codes to their numeric values
@@ -268,8 +268,8 @@ const validateTaiwanNationalId = (value: string, type: NationalIdType = "both",
268
268
  * Creates a Zod schema for Taiwan National ID validation
269
269
  *
270
270
  * @template IsRequired - Whether the field is required (affects return type)
271
- * @param {NationalIdOptions<IsRequired>} [options] - Configuration options for national ID validation
272
- * @returns {NationalIdSchema<IsRequired>} Zod schema for national ID validation
271
+ * @param {TwNationalIdOptions<IsRequired>} [options] - Configuration options for national ID validation
272
+ * @returns {TwNationalIdSchema<IsRequired>} Zod schema for national ID validation
273
273
  *
274
274
  * @description
275
275
  * Creates a comprehensive Taiwan National ID validator that supports both
@@ -288,18 +288,18 @@ const validateTaiwanNationalId = (value: string, type: NationalIdType = "both",
288
288
  * @example
289
289
  * ```typescript
290
290
  * // Accept any valid Taiwan ID
291
- * const anyIdSchema = nationalId()
291
+ * const anyIdSchema = twNationalId()
292
292
  * anyIdSchema.parse("A123456789") // ✓ Valid citizen ID
293
293
  * anyIdSchema.parse("A812345678") // ✓ Valid new resident ID
294
294
  * anyIdSchema.parse("AA12345678") // ✓ Valid old resident ID
295
295
  *
296
296
  * // Citizen IDs only
297
- * const citizenSchema = nationalId(false, { type: "citizen" })
297
+ * const citizenSchema = twNationalId(false, { type: "citizen" })
298
298
  * citizenSchema.parse("A123456789") // ✓ Valid
299
299
  * citizenSchema.parse("A812345678") // ✗ Invalid (resident ID)
300
300
  *
301
301
  * // Resident IDs only (new format only)
302
- * const residentSchema = nationalId(false, {
302
+ * const residentSchema = twNationalId(false, {
303
303
  * type: "resident",
304
304
  * allowOldResident: false
305
305
  * })
@@ -307,12 +307,12 @@ const validateTaiwanNationalId = (value: string, type: NationalIdType = "both",
307
307
  * residentSchema.parse("AA12345678") // ✗ Invalid (old format)
308
308
  *
309
309
  * // Optional with custom transformation
310
- * const optionalSchema = nationalId(false, {
310
+ * const optionalSchema = twNationalId(false, {
311
311
  * transform: (value) => value.replace(/[^A-Z0-9]/g, '') // Remove special chars
312
312
  * })
313
313
  *
314
314
  * // With custom error messages
315
- * const customSchema = nationalId(false, {
315
+ * const customSchema = twNationalId(false, {
316
316
  * i18n: {
317
317
  * en: { invalid: "Please enter a valid Taiwan National ID" },
318
318
  * 'zh-TW': { invalid: "請輸入有效的身分證或居留證號碼" }
@@ -321,11 +321,11 @@ const validateTaiwanNationalId = (value: string, type: NationalIdType = "both",
321
321
  * ```
322
322
  *
323
323
  * @throws {z.ZodError} When validation fails with specific error messages
324
- * @see {@link NationalIdOptions} for all available configuration options
324
+ * @see {@link TwNationalIdOptions} for all available configuration options
325
325
  * @see {@link NationalIdType} for supported ID types
326
326
  * @see {@link validateTaiwanNationalId} for validation logic details
327
327
  */
328
- export function nationalId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<NationalIdOptions<IsRequired>, 'required'>): NationalIdSchema<IsRequired> {
328
+ export function twNationalId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwNationalIdOptions<IsRequired>, 'required'>): TwNationalIdSchema<IsRequired> {
329
329
  const {
330
330
  type = "both",
331
331
  allowOldResident = true,
@@ -340,7 +340,7 @@ export function nationalId<IsRequired extends boolean = false>(required?: IsRequ
340
340
  const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
341
341
 
342
342
  // Helper function to get custom message or fallback to default i18n
343
- const getMessage = (key: keyof NationalIdMessages, params?: Record<string, any>) => {
343
+ const getMessage = (key: keyof TwNationalIdMessages, params?: Record<string, any>) => {
344
344
  if (i18n) {
345
345
  const currentLocale = getLocale()
346
346
  const customMessages = i18n[currentLocale]
@@ -374,26 +374,26 @@ export function nationalId<IsRequired extends boolean = false>(required?: IsRequ
374
374
 
375
375
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
376
376
 
377
- const schema = baseSchema.refine((val) => {
378
- if (val === null) return true
377
+ const schema = baseSchema.superRefine((val, ctx) => {
378
+ if (val === null) return
379
379
 
380
380
  // Required check
381
381
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
382
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
382
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
383
+ return
383
384
  }
384
385
 
385
- if (val === null) return true
386
- if (!isRequired && val === "") return true
386
+ if (val === null) return
387
+ if (!isRequired && val === "") return
387
388
 
388
389
  // Taiwan National ID validation
389
390
  if (!validateTaiwanNationalId(val, type, allowOldResident)) {
390
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
391
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
392
+ return
391
393
  }
392
-
393
- return true
394
394
  })
395
395
 
396
- return schema as unknown as NationalIdSchema<IsRequired>
396
+ return schema as unknown as TwNationalIdSchema<IsRequired>
397
397
  }
398
398
 
399
399
  /**