@hy_ong/zod-kit 0.1.2 → 0.1.4

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.
@@ -253,12 +253,13 @@ export function url<IsRequired extends boolean = false>(required?: IsRequired, o
253
253
 
254
254
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
255
255
 
256
- const schema = baseSchema.refine((val) => {
257
- if (val === null) return true
256
+ const schema = baseSchema.superRefine((val, ctx) => {
257
+ if (val === null) return
258
258
 
259
259
  // Required check
260
260
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
261
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
261
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
262
+ return
262
263
  }
263
264
 
264
265
  // URL format validation
@@ -266,88 +267,104 @@ export function url<IsRequired extends boolean = false>(required?: IsRequired, o
266
267
  try {
267
268
  urlObj = new URL(val)
268
269
  } catch {
269
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
270
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
271
+ return
270
272
  }
271
273
 
272
274
  // Length checks
273
275
  if (val !== null && min !== undefined && val.length < min) {
274
- throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
276
+ ctx.addIssue({ code: "custom", message: getMessage("min", { min }) })
277
+ return
275
278
  }
276
279
  if (val !== null && max !== undefined && val.length > max) {
277
- throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
280
+ ctx.addIssue({ code: "custom", message: getMessage("max", { max }) })
281
+ return
278
282
  }
279
283
 
280
284
  // String content checks
281
285
  if (val !== null && includes !== undefined && !val.includes(includes)) {
282
- throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
286
+ ctx.addIssue({ code: "custom", message: getMessage("includes", { includes }) })
287
+ return
283
288
  }
284
289
  if (val !== null && excludes !== undefined) {
285
290
  const excludeList = Array.isArray(excludes) ? excludes : [excludes]
286
291
  for (const exclude of excludeList) {
287
292
  if (val.includes(exclude)) {
288
- throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
293
+ ctx.addIssue({ code: "custom", message: getMessage("excludes", { excludes: exclude }) })
294
+ return
289
295
  }
290
296
  }
291
297
  }
292
298
 
293
299
  // Protocol validation
294
300
  if (protocols && !protocols.includes(urlObj.protocol.slice(0, -1))) {
295
- throw new z.ZodError([{ code: "custom", message: getMessage("protocol", { protocols: protocols.join(", ") }), path: [] }])
301
+ ctx.addIssue({ code: "custom", message: getMessage("protocol", { protocols: protocols.join(", ") }) })
302
+ return
296
303
  }
297
304
 
298
305
  // Domain validation
299
306
  const hostname = urlObj.hostname.toLowerCase()
300
307
  if (allowedDomains && !allowedDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`))) {
301
- throw new z.ZodError([{ code: "custom", message: getMessage("domain", { domains: allowedDomains.join(", ") }), path: [] }])
308
+ ctx.addIssue({ code: "custom", message: getMessage("domain", { domains: allowedDomains.join(", ") }) })
309
+ return
302
310
  }
303
311
  if (blockedDomains && blockedDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`))) {
304
312
  const blockedDomain = blockedDomains.find((domain) => hostname === domain || hostname.endsWith(`.${domain}`))
305
- throw new z.ZodError([{ code: "custom", message: getMessage("domainBlacklist", { domain: blockedDomain }), path: [] }])
313
+ ctx.addIssue({ code: "custom", message: getMessage("domainBlacklist", { domain: blockedDomain }) })
314
+ return
306
315
  }
307
316
 
308
317
  // Port validation
309
318
  const port = urlObj.port ? parseInt(urlObj.port) : urlObj.protocol === "https:" ? 443 : 80
310
319
  if (allowedPorts && !allowedPorts.includes(port)) {
311
- throw new z.ZodError([{ code: "custom", message: getMessage("port", { ports: allowedPorts.join(", ") }), path: [] }])
320
+ ctx.addIssue({ code: "custom", message: getMessage("port", { ports: allowedPorts.join(", ") }) })
321
+ return
312
322
  }
313
323
  if (blockedPorts && blockedPorts.includes(port)) {
314
- throw new z.ZodError([{ code: "custom", message: getMessage("port", { port }), path: [] }])
324
+ ctx.addIssue({ code: "custom", message: getMessage("port", { port }) })
325
+ return
315
326
  }
316
327
 
317
328
  // Path validation
318
329
  if (pathStartsWith && !urlObj.pathname.startsWith(pathStartsWith)) {
319
- throw new z.ZodError([{ code: "custom", message: getMessage("pathStartsWith", { path: pathStartsWith }), path: [] }])
330
+ ctx.addIssue({ code: "custom", message: getMessage("pathStartsWith", { path: pathStartsWith }) })
331
+ return
320
332
  }
321
333
  if (pathEndsWith && !urlObj.pathname.endsWith(pathEndsWith)) {
322
- throw new z.ZodError([{ code: "custom", message: getMessage("pathEndsWith", { path: pathEndsWith }), path: [] }])
334
+ ctx.addIssue({ code: "custom", message: getMessage("pathEndsWith", { path: pathEndsWith }) })
335
+ return
323
336
  }
324
337
 
325
338
  // Query validation
326
339
  if (mustHaveQuery && !urlObj.search) {
327
- throw new z.ZodError([{ code: "custom", message: getMessage("hasQuery"), path: [] }])
340
+ ctx.addIssue({ code: "custom", message: getMessage("hasQuery") })
341
+ return
328
342
  }
329
343
  if (mustNotHaveQuery && urlObj.search) {
330
- throw new z.ZodError([{ code: "custom", message: getMessage("noQuery"), path: [] }])
344
+ ctx.addIssue({ code: "custom", message: getMessage("noQuery") })
345
+ return
331
346
  }
332
347
 
333
348
  // Fragment validation
334
349
  if (mustHaveFragment && !urlObj.hash) {
335
- throw new z.ZodError([{ code: "custom", message: getMessage("hasFragment"), path: [] }])
350
+ ctx.addIssue({ code: "custom", message: getMessage("hasFragment") })
351
+ return
336
352
  }
337
353
  if (mustNotHaveFragment && urlObj.hash) {
338
- throw new z.ZodError([{ code: "custom", message: getMessage("noFragment"), path: [] }])
354
+ ctx.addIssue({ code: "custom", message: getMessage("noFragment") })
355
+ return
339
356
  }
340
357
 
341
358
  // Localhost validation
342
359
  const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
343
360
  if (blockLocalhost && isLocalhost) {
344
- throw new z.ZodError([{ code: "custom", message: getMessage("noLocalhost"), path: [] }])
361
+ ctx.addIssue({ code: "custom", message: getMessage("noLocalhost") })
362
+ return
345
363
  }
346
364
  if (!allowLocalhost && isLocalhost) {
347
- throw new z.ZodError([{ code: "custom", message: getMessage("localhost"), path: [] }])
365
+ ctx.addIssue({ code: "custom", message: getMessage("localhost") })
366
+ return
348
367
  }
349
-
350
- return true
351
368
  })
352
369
 
353
370
  return schema as unknown as UrlSchema<IsRequired>
@@ -15,11 +15,11 @@ import { getLocale, type Locale } from "../../config"
15
15
  /**
16
16
  * Type definition for business ID validation error messages
17
17
  *
18
- * @interface BusinessIdMessages
18
+ * @interface TwBusinessIdMessages
19
19
  * @property {string} [required] - Message when field is required but empty
20
20
  * @property {string} [invalid] - Message when business ID format or checksum is invalid
21
21
  */
22
- export type BusinessIdMessages = {
22
+ export type TwBusinessIdMessages = {
23
23
  required?: string
24
24
  invalid?: string
25
25
  }
@@ -29,26 +29,26 @@ export type BusinessIdMessages = {
29
29
  *
30
30
  * @template IsRequired - Whether the field is required (affects return type)
31
31
  *
32
- * @interface BusinessIdOptions
32
+ * @interface TwBusinessIdOptions
33
33
  * @property {IsRequired} [required=true] - Whether the field is required
34
34
  * @property {Function} [transform] - Custom transformation function for business ID
35
35
  * @property {string | null} [defaultValue] - Default value when input is empty
36
- * @property {Record<Locale, BusinessIdMessages>} [i18n] - Custom error messages for different locales
36
+ * @property {Record<Locale, TwBusinessIdMessages>} [i18n] - Custom error messages for different locales
37
37
  */
38
- export type BusinessIdOptions<IsRequired extends boolean = true> = {
38
+ export type TwBusinessIdOptions<IsRequired extends boolean = true> = {
39
39
  transform?: (value: string) => string
40
40
  defaultValue?: IsRequired extends true ? string : string | null
41
- i18n?: Record<Locale, BusinessIdMessages>
41
+ i18n?: Record<Locale, TwBusinessIdMessages>
42
42
  }
43
43
 
44
44
  /**
45
45
  * Type alias for business ID validation schema based on required flag
46
46
  *
47
47
  * @template IsRequired - Whether the field is required
48
- * @typedef BusinessIdSchema
48
+ * @typedef TwBusinessIdSchema
49
49
  * @description Returns ZodString if required, ZodNullable<ZodString> if optional
50
50
  */
51
- export type BusinessIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
51
+ export type TwBusinessIdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
52
52
 
53
53
  /**
54
54
  * Validates Taiwan Business Identification Number (統一編號)
@@ -80,7 +80,7 @@ const validateTaiwanBusinessId = (value: string): boolean => {
80
80
  return false
81
81
  }
82
82
 
83
- const digits = value.split('').map(Number)
83
+ const digits = value.split("").map(Number)
84
84
 
85
85
  // Coefficients for the first 7 digits
86
86
  const coefficients = [1, 2, 1, 2, 1, 2, 4]
@@ -179,23 +179,19 @@ const validateTaiwanBusinessId = (value: string): boolean => {
179
179
  * ```
180
180
  *
181
181
  * @throws {z.ZodError} When validation fails with specific error messages
182
- * @see {@link BusinessIdOptions} for all available configuration options
182
+ * @see {@link TwBusinessIdOptions} for all available configuration options
183
183
  * @see {@link validateTaiwanBusinessId} for validation logic details
184
184
  */
185
- export function businessId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<BusinessIdOptions<IsRequired>, 'required'>): BusinessIdSchema<IsRequired> {
186
- const {
187
- transform,
188
- defaultValue,
189
- i18n
190
- } = options ?? {}
185
+ export function twBusinessId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwBusinessIdOptions<IsRequired>, "required">): TwBusinessIdSchema<IsRequired> {
186
+ const { transform, defaultValue, i18n } = options ?? {}
191
187
 
192
- const isRequired = required ?? false as IsRequired
188
+ const isRequired = required ?? (false as IsRequired)
193
189
 
194
190
  // Set appropriate default value based on required flag
195
191
  const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
196
192
 
197
193
  // Helper function to get custom message or fallback to default i18n
198
- const getMessage = (key: keyof BusinessIdMessages, params?: Record<string, any>) => {
194
+ const getMessage = (key: keyof TwBusinessIdMessages, params?: Record<string, any>) => {
199
195
  if (i18n) {
200
196
  const currentLocale = getLocale()
201
197
  const customMessages = i18n[currentLocale]
@@ -229,26 +225,26 @@ export function businessId<IsRequired extends boolean = false>(required?: IsRequ
229
225
 
230
226
  const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
231
227
 
232
- const schema = baseSchema.refine((val) => {
233
- if (val === null) return true
228
+ const schema = baseSchema.superRefine((val, ctx) => {
229
+ if (val === null) return
234
230
 
235
231
  // Required check
236
232
  if (isRequired && (val === "" || val === "null" || val === "undefined")) {
237
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
233
+ ctx.addIssue({ code: "custom", message: getMessage("required") })
234
+ return
238
235
  }
239
236
 
240
- if (val === null) return true
241
- if (!isRequired && val === "") return true
237
+ if (val === null) return
238
+ if (!isRequired && val === "") return
242
239
 
243
240
  // Taiwan Business ID format validation (8 digits + checksum)
244
241
  if (!validateTaiwanBusinessId(val)) {
245
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
242
+ ctx.addIssue({ code: "custom", message: getMessage("invalid") })
243
+ return
246
244
  }
247
-
248
- return true
249
245
  })
250
246
 
251
- return schema as unknown as BusinessIdSchema<IsRequired>
247
+ return schema as unknown as TwBusinessIdSchema<IsRequired>
252
248
  }
253
249
 
254
250
  /**
@@ -266,4 +262,4 @@ export function businessId<IsRequired extends boolean = false>(required?: IsRequ
266
262
  * const isValid = validateTaiwanBusinessId("12345675") // boolean
267
263
  * ```
268
264
  */
269
- export { validateTaiwanBusinessId }
265
+ export { validateTaiwanBusinessId }
@@ -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
  /**