@hy_ong/zod-kit 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5NBWPAHC.cjs +77 -0
- package/dist/{chunk-BZWQPSHO.cjs → chunk-5U5ERV2P.cjs} +16 -28
- package/dist/{chunk-7X3XPK6A.js → chunk-S4OJGXWC.js} +16 -28
- package/dist/chunk-YG6YHVAE.js +77 -0
- package/dist/common/many-of.cjs +2 -2
- package/dist/common/many-of.d.cts +18 -14
- package/dist/common/many-of.d.ts +18 -14
- package/dist/common/many-of.js +1 -1
- package/dist/common/one-of.cjs +2 -2
- package/dist/common/one-of.d.cts +21 -15
- package/dist/common/one-of.d.ts +21 -15
- package/dist/common/one-of.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/validators/common/many-of.ts +84 -79
- package/src/validators/common/one-of.ts +97 -55
- package/dist/chunk-3ZF5JO3F.js +0 -61
- package/dist/chunk-FMPHW7ID.cjs +0 -61
package/dist/common/one-of.d.ts
CHANGED
|
@@ -7,8 +7,11 @@ import { L as Locale } from '../config-CABSSvAp.js';
|
|
|
7
7
|
* Provides single-select validation that restricts input to a predefined set of allowed values,
|
|
8
8
|
* with support for case-insensitive matching, default values, and transformation.
|
|
9
9
|
*
|
|
10
|
+
* Uses z.enum() internally to preserve literal type inference for both z.input and z.output,
|
|
11
|
+
* making it compatible with React Hook Form resolvers and other type-aware form libraries.
|
|
12
|
+
*
|
|
10
13
|
* @author Ong Hoe Yuan
|
|
11
|
-
* @version 0.2.
|
|
14
|
+
* @version 0.2.5
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -26,37 +29,40 @@ type OneOfMessages = {
|
|
|
26
29
|
* Configuration options for oneOf validation
|
|
27
30
|
*
|
|
28
31
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
29
|
-
* @template
|
|
32
|
+
* @template V - The tuple type of allowed values, preserving literal types
|
|
30
33
|
*
|
|
31
34
|
* @interface OneOfOptions
|
|
32
|
-
* @property {
|
|
33
|
-
* @property {
|
|
35
|
+
* @property {V} values - Array of allowed values
|
|
36
|
+
* @property {V[number] | null} [defaultValue] - Default value when input is empty
|
|
34
37
|
* @property {boolean} [caseSensitive=true] - Whether string matching is case-sensitive
|
|
35
38
|
* @property {Function} [transform] - Custom transformation function applied after validation
|
|
36
39
|
* @property {Record<Locale, OneOfMessages>} [i18n] - Custom error messages for different locales
|
|
37
40
|
*/
|
|
38
|
-
type OneOfOptions<IsRequired extends boolean = true,
|
|
39
|
-
values:
|
|
40
|
-
defaultValue?: IsRequired extends true ?
|
|
41
|
+
type OneOfOptions<IsRequired extends boolean = true, V extends readonly (string | number)[] = readonly (string | number)[]> = {
|
|
42
|
+
values: V;
|
|
43
|
+
defaultValue?: IsRequired extends true ? V[number] : V[number] | null;
|
|
41
44
|
caseSensitive?: boolean;
|
|
42
|
-
transform?: (value:
|
|
45
|
+
transform?: (value: V[number]) => V[number];
|
|
43
46
|
i18n?: Partial<Record<Locale, Partial<OneOfMessages>>>;
|
|
44
47
|
};
|
|
45
48
|
/**
|
|
46
49
|
* Type alias for oneOf validation schema based on required flag
|
|
47
50
|
*
|
|
48
51
|
* @template IsRequired - Whether the field is required
|
|
49
|
-
* @template
|
|
52
|
+
* @template V - The tuple type of allowed values
|
|
50
53
|
*/
|
|
51
|
-
type OneOfSchema<IsRequired extends boolean,
|
|
54
|
+
type OneOfSchema<IsRequired extends boolean, V extends readonly (string | number)[]> = IsRequired extends true ? ZodType<V[number], V[number] | "" | null | undefined> : ZodType<V[number] | null, V[number] | "" | null | undefined>;
|
|
52
55
|
/**
|
|
53
56
|
* Creates a Zod schema for single-select validation that restricts values to a predefined set
|
|
54
57
|
*
|
|
58
|
+
* Uses z.enum() internally to preserve both z.input and z.output literal types,
|
|
59
|
+
* ensuring compatibility with React Hook Form and other type-aware form libraries.
|
|
60
|
+
*
|
|
55
61
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
56
|
-
* @template
|
|
62
|
+
* @template V - The tuple type of allowed values (inferred via const type parameter)
|
|
57
63
|
* @param {IsRequired} [required=false] - Whether the field is required
|
|
58
|
-
* @param {OneOfOptions<IsRequired,
|
|
59
|
-
* @returns {OneOfSchema<IsRequired,
|
|
64
|
+
* @param {OneOfOptions<IsRequired, V>} options - Configuration options (values is required)
|
|
65
|
+
* @returns {OneOfSchema<IsRequired, V>} Zod schema for oneOf validation
|
|
60
66
|
*
|
|
61
67
|
* @example
|
|
62
68
|
* ```typescript
|
|
@@ -65,7 +71,7 @@ type OneOfSchema<IsRequired extends boolean, T> = IsRequired extends true ? ZodT
|
|
|
65
71
|
* roleSchema.parse("admin") // ✓ "admin"
|
|
66
72
|
* roleSchema.parse(null) // ✓ null
|
|
67
73
|
*
|
|
68
|
-
* // Required
|
|
74
|
+
* // Required — z.input and z.output are both "active" | "inactive" | "pending"
|
|
69
75
|
* const statusSchema = oneOf(true, { values: ["active", "inactive", "pending"] })
|
|
70
76
|
* statusSchema.parse("active") // ✓ "active"
|
|
71
77
|
* statusSchema.parse(null) // ✗ Required
|
|
@@ -99,6 +105,6 @@ type OneOfSchema<IsRequired extends boolean, T> = IsRequired extends true ? ZodT
|
|
|
99
105
|
* sizeSchema.parse("m") // ✓ "M"
|
|
100
106
|
* ```
|
|
101
107
|
*/
|
|
102
|
-
declare function oneOf<IsRequired extends boolean = false,
|
|
108
|
+
declare function oneOf<IsRequired extends boolean = false, const V extends readonly (string | number)[] = readonly (string | number)[]>(required?: IsRequired, options?: Omit<OneOfOptions<IsRequired, V>, "required">): OneOfSchema<IsRequired, V>;
|
|
103
109
|
|
|
104
110
|
export { type OneOfMessages, type OneOfOptions, type OneOfSchema, oneOf };
|
package/dist/common/one-of.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -60,7 +60,7 @@ var _chunkBZSPJJYTcjs = require('./chunk-BZSPJJYT.cjs');
|
|
|
60
60
|
var _chunkG747FHUZcjs = require('./chunk-G747FHUZ.cjs');
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
var
|
|
63
|
+
var _chunk5NBWPAHCcjs = require('./chunk-5NBWPAHC.cjs');
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
var _chunkQ24GYUTOcjs = require('./chunk-Q24GYUTO.cjs');
|
|
@@ -114,7 +114,7 @@ var _chunkKIUO2HIRcjs = require('./chunk-KIUO2HIR.cjs');
|
|
|
114
114
|
var _chunkJZEF5Q3Wcjs = require('./chunk-JZEF5Q3W.cjs');
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
var
|
|
117
|
+
var _chunk5U5ERV2Pcjs = require('./chunk-5U5ERV2P.cjs');
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
|
|
@@ -185,4 +185,4 @@ var _chunkQ7TUNJD4cjs = require('./chunk-Q7TUNJD4.cjs');
|
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
exports.DATETIME_PATTERNS = _chunk53EEWALQcjs.DATETIME_PATTERNS; exports.ID_PATTERNS = _chunkJZEF5Q3Wcjs.ID_PATTERNS; exports.TAIWAN_BANK_CODES = _chunkQQWX3ICKcjs.TAIWAN_BANK_CODES; exports.TIME_PATTERNS = _chunkEDTNS2XLcjs.TIME_PATTERNS; exports.VALID_3_DIGIT_PREFIXES = _chunkFP4O2ICMcjs.VALID_3_DIGIT_PREFIXES; exports.boolean = _chunkLNWEJED7cjs.boolean; exports.color = _chunkA2GAEU4Ocjs.color; exports.coordinate = _chunkFEL432I2cjs.coordinate; exports.creditCard = _chunkZCX22PY4cjs.creditCard; exports.date = _chunk4AQB4RSUcjs.date; exports.datetime = _chunk53EEWALQcjs.datetime; exports.detectCardType = _chunkZCX22PY4cjs.detectCardType; exports.detectIdType = _chunkJZEF5Q3Wcjs.detectIdType; exports.email = _chunkH25N5GP6cjs.email; exports.file = _chunkKIUO2HIRcjs.file; exports.getLocale = _chunkQ7TUNJD4cjs.getLocale; exports.id = _chunkJZEF5Q3Wcjs.id; exports.ip = _chunkLXFRQLH4cjs.ip; exports.manyOf =
|
|
188
|
+
exports.DATETIME_PATTERNS = _chunk53EEWALQcjs.DATETIME_PATTERNS; exports.ID_PATTERNS = _chunkJZEF5Q3Wcjs.ID_PATTERNS; exports.TAIWAN_BANK_CODES = _chunkQQWX3ICKcjs.TAIWAN_BANK_CODES; exports.TIME_PATTERNS = _chunkEDTNS2XLcjs.TIME_PATTERNS; exports.VALID_3_DIGIT_PREFIXES = _chunkFP4O2ICMcjs.VALID_3_DIGIT_PREFIXES; exports.boolean = _chunkLNWEJED7cjs.boolean; exports.color = _chunkA2GAEU4Ocjs.color; exports.coordinate = _chunkFEL432I2cjs.coordinate; exports.creditCard = _chunkZCX22PY4cjs.creditCard; exports.date = _chunk4AQB4RSUcjs.date; exports.datetime = _chunk53EEWALQcjs.datetime; exports.detectCardType = _chunkZCX22PY4cjs.detectCardType; exports.detectIdType = _chunkJZEF5Q3Wcjs.detectIdType; exports.email = _chunkH25N5GP6cjs.email; exports.file = _chunkKIUO2HIRcjs.file; exports.getLocale = _chunkQ7TUNJD4cjs.getLocale; exports.id = _chunkJZEF5Q3Wcjs.id; exports.ip = _chunkLXFRQLH4cjs.ip; exports.manyOf = _chunk5U5ERV2Pcjs.manyOf; exports.normalizeDateTimeValue = _chunk53EEWALQcjs.normalizeDateTimeValue; exports.normalizeTime = _chunkEDTNS2XLcjs.normalizeTime; exports.number = _chunkG747FHUZcjs.number; exports.oneOf = _chunk5NBWPAHCcjs.oneOf; exports.parseDateTimeValue = _chunk53EEWALQcjs.parseDateTimeValue; exports.parseTimeToMinutes = _chunkEDTNS2XLcjs.parseTimeToMinutes; exports.password = _chunkQ24GYUTOcjs.password; exports.setLocale = _chunkQ7TUNJD4cjs.setLocale; exports.text = _chunkHZ2WESSLcjs.text; exports.time = _chunkEDTNS2XLcjs.time; exports.twBankAccount = _chunkQQWX3ICKcjs.twBankAccount; exports.twBusinessId = _chunkIWR3H7IHcjs.twBusinessId; exports.twFax = _chunkBZSPJJYTcjs.twFax; exports.twInvoice = _chunkV2KKGSKQcjs.twInvoice; exports.twLicensePlate = _chunkTPXRQT2Hcjs.twLicensePlate; exports.twMobile = _chunkAYCXAJRAcjs.twMobile; exports.twNationalId = _chunkUCPKW43Kcjs.twNationalId; exports.twPassport = _chunk5ZMTAI4Gcjs.twPassport; exports.twPostalCode = _chunkFP4O2ICMcjs.twPostalCode; exports.twTel = _chunkVKBNKPFOcjs.twTel; exports.url = _chunkTDEXEIHHcjs.url; exports.validate3DigitPostalCode = _chunkFP4O2ICMcjs.validate3DigitPostalCode; exports.validate5DigitPostalCode = _chunkFP4O2ICMcjs.validate5DigitPostalCode; exports.validate6DigitPostalCode = _chunkFP4O2ICMcjs.validate6DigitPostalCode; exports.validateCitizenId = _chunkUCPKW43Kcjs.validateCitizenId; exports.validateColor = _chunkA2GAEU4Ocjs.validateColor; exports.validateCreditCard = _chunkZCX22PY4cjs.validateCreditCard; exports.validateDateTimeFormat = _chunk53EEWALQcjs.validateDateTimeFormat; exports.validateIPv4 = _chunkLXFRQLH4cjs.validateIPv4; exports.validateIPv6 = _chunkLXFRQLH4cjs.validateIPv6; exports.validateIdType = _chunkJZEF5Q3Wcjs.validateIdType; exports.validateLatitude = _chunkFEL432I2cjs.validateLatitude; exports.validateLongitude = _chunkFEL432I2cjs.validateLongitude; exports.validateNewResidentId = _chunkUCPKW43Kcjs.validateNewResidentId; exports.validateOldResidentId = _chunkUCPKW43Kcjs.validateOldResidentId; exports.validateTaiwanBankAccount = _chunkQQWX3ICKcjs.validateTaiwanBankAccount; exports.validateTaiwanBusinessId = _chunkIWR3H7IHcjs.validateTaiwanBusinessId; exports.validateTaiwanFax = _chunkBZSPJJYTcjs.validateTaiwanFax; exports.validateTaiwanInvoice = _chunkV2KKGSKQcjs.validateTaiwanInvoice; exports.validateTaiwanLicensePlate = _chunkTPXRQT2Hcjs.validateTaiwanLicensePlate; exports.validateTaiwanMobile = _chunkAYCXAJRAcjs.validateTaiwanMobile; exports.validateTaiwanNationalId = _chunkUCPKW43Kcjs.validateTaiwanNationalId; exports.validateTaiwanPassport = _chunk5ZMTAI4Gcjs.validateTaiwanPassport; exports.validateTaiwanPostalCode = _chunkFP4O2ICMcjs.validateTaiwanPostalCode; exports.validateTaiwanTel = _chunkVKBNKPFOcjs.validateTaiwanTel; exports.validateTimeFormat = _chunkEDTNS2XLcjs.validateTimeFormat;
|
package/dist/index.js
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./chunk-3QLUXIY2.js";
|
|
61
61
|
import {
|
|
62
62
|
oneOf
|
|
63
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-YG6YHVAE.js";
|
|
64
64
|
import {
|
|
65
65
|
password
|
|
66
66
|
} from "./chunk-NWQSOSNF.js";
|
|
@@ -114,7 +114,7 @@ import {
|
|
|
114
114
|
} from "./chunk-H6STFX4I.js";
|
|
115
115
|
import {
|
|
116
116
|
manyOf
|
|
117
|
-
} from "./chunk-
|
|
117
|
+
} from "./chunk-S4OJGXWC.js";
|
|
118
118
|
import {
|
|
119
119
|
getLocale,
|
|
120
120
|
setLocale
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hy_ong/zod-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "A comprehensive TypeScript library providing pre-built Zod validation schemas with full internationalization support for common data types and Taiwan-specific formats",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zod",
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Provides multi-select validation that restricts input to an array of values
|
|
5
5
|
* from a predefined set, with min/max selection, duplicate control, and transformation.
|
|
6
6
|
*
|
|
7
|
+
* Avoids z.preprocess() to preserve z.input types for React Hook Form compatibility.
|
|
8
|
+
*
|
|
7
9
|
* @author Ong Hoe Yuan
|
|
8
|
-
* @version 0.2.
|
|
10
|
+
* @version 0.2.5
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { z, ZodType } from "zod"
|
|
@@ -34,11 +36,11 @@ export type ManyOfMessages = {
|
|
|
34
36
|
* Configuration options for manyOf validation
|
|
35
37
|
*
|
|
36
38
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
37
|
-
* @template
|
|
39
|
+
* @template V - The tuple type of allowed values, preserving literal types
|
|
38
40
|
*
|
|
39
41
|
* @interface ManyOfOptions
|
|
40
|
-
* @property {
|
|
41
|
-
* @property {
|
|
42
|
+
* @property {V} values - Array of allowed values
|
|
43
|
+
* @property {V[number][] | null} [defaultValue] - Default value when input is empty
|
|
42
44
|
* @property {number} [min] - Minimum number of selections
|
|
43
45
|
* @property {number} [max] - Maximum number of selections
|
|
44
46
|
* @property {boolean} [allowDuplicates=false] - Whether to allow duplicate selections
|
|
@@ -46,14 +48,14 @@ export type ManyOfMessages = {
|
|
|
46
48
|
* @property {Function} [transform] - Custom transformation function applied to each value
|
|
47
49
|
* @property {Record<Locale, ManyOfMessages>} [i18n] - Custom error messages for different locales
|
|
48
50
|
*/
|
|
49
|
-
export type ManyOfOptions<IsRequired extends boolean = true,
|
|
50
|
-
values:
|
|
51
|
-
defaultValue?: IsRequired extends true ?
|
|
51
|
+
export type ManyOfOptions<IsRequired extends boolean = true, V extends readonly (string | number)[] = readonly (string | number)[]> = {
|
|
52
|
+
values: V
|
|
53
|
+
defaultValue?: IsRequired extends true ? V[number][] : V[number][] | null
|
|
52
54
|
min?: number
|
|
53
55
|
max?: number
|
|
54
56
|
allowDuplicates?: boolean
|
|
55
57
|
caseSensitive?: boolean
|
|
56
|
-
transform?: (value:
|
|
58
|
+
transform?: (value: V[number][]) => V[number][]
|
|
57
59
|
i18n?: Partial<Record<Locale, Partial<ManyOfMessages>>>
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -61,18 +63,22 @@ export type ManyOfOptions<IsRequired extends boolean = true, T extends string |
|
|
|
61
63
|
* Type alias for manyOf validation schema based on required flag
|
|
62
64
|
*
|
|
63
65
|
* @template IsRequired - Whether the field is required
|
|
64
|
-
* @template
|
|
66
|
+
* @template V - The tuple type of allowed values
|
|
65
67
|
*/
|
|
66
|
-
export type ManyOfSchema<IsRequired extends boolean,
|
|
68
|
+
export type ManyOfSchema<IsRequired extends boolean, V extends readonly (string | number)[]> = IsRequired extends true
|
|
69
|
+
? ZodType<V[number][], V[number][] | "" | null | undefined>
|
|
70
|
+
: ZodType<V[number][] | null, V[number][] | "" | null | undefined>
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
73
|
* Creates a Zod schema for multi-select validation that restricts values to a predefined set
|
|
70
74
|
*
|
|
75
|
+
* Avoids z.preprocess() to preserve z.input types for React Hook Form compatibility.
|
|
76
|
+
*
|
|
71
77
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
72
|
-
* @template
|
|
78
|
+
* @template V - The tuple type of allowed values (inferred via const type parameter)
|
|
73
79
|
* @param {IsRequired} [required=false] - Whether the field is required
|
|
74
|
-
* @param {ManyOfOptions<IsRequired,
|
|
75
|
-
* @returns {ManyOfSchema<IsRequired,
|
|
80
|
+
* @param {ManyOfOptions<IsRequired, V>} options - Configuration options (values is required)
|
|
81
|
+
* @returns {ManyOfSchema<IsRequired, V>} Zod schema for manyOf validation
|
|
76
82
|
*
|
|
77
83
|
* @example
|
|
78
84
|
* ```typescript
|
|
@@ -110,13 +116,14 @@ export type ManyOfSchema<IsRequired extends boolean, T> = IsRequired extends tru
|
|
|
110
116
|
* itemsSchema.parse([1, 1, 2]) // ✓ [1, 1, 2]
|
|
111
117
|
* ```
|
|
112
118
|
*/
|
|
113
|
-
export function manyOf<IsRequired extends boolean = false,
|
|
119
|
+
export function manyOf<IsRequired extends boolean = false, const V extends readonly (string | number)[] = readonly (string | number)[]>(
|
|
114
120
|
required?: IsRequired,
|
|
115
|
-
options?: Omit<ManyOfOptions<IsRequired,
|
|
116
|
-
): ManyOfSchema<IsRequired,
|
|
117
|
-
const { values = [] as unknown as
|
|
121
|
+
options?: Omit<ManyOfOptions<IsRequired, V>, "required">,
|
|
122
|
+
): ManyOfSchema<IsRequired, V> {
|
|
123
|
+
const { values = [] as unknown as V, defaultValue = null, min, max, allowDuplicates = false, caseSensitive = true, transform, i18n } = options ?? {}
|
|
118
124
|
|
|
119
125
|
const isRequired = required ?? (false as IsRequired)
|
|
126
|
+
const valuesArr = values as readonly (string | number)[]
|
|
120
127
|
|
|
121
128
|
const getMessage = (key: keyof ManyOfMessages, params?: Record<string, any>) => {
|
|
122
129
|
if (i18n) {
|
|
@@ -132,15 +139,15 @@ export function manyOf<IsRequired extends boolean = false, T extends string | nu
|
|
|
132
139
|
|
|
133
140
|
const normalizeItem = (item: unknown): unknown => {
|
|
134
141
|
// Coerce number strings to numbers when values contains numbers
|
|
135
|
-
const hasNumbers =
|
|
142
|
+
const hasNumbers = valuesArr.some((v) => typeof v === "number")
|
|
136
143
|
if (hasNumbers && typeof item === "string" && !isNaN(Number(item)) && item.trim() !== "") {
|
|
137
144
|
const numVal = Number(item)
|
|
138
|
-
if ((
|
|
145
|
+
if ((valuesArr as readonly number[]).includes(numVal)) return numVal
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
// Case-insensitive normalization
|
|
142
149
|
if (!caseSensitive && typeof item === "string") {
|
|
143
|
-
const match =
|
|
150
|
+
const match = valuesArr.find((v) => typeof v === "string" && v.toLowerCase() === item.toLowerCase())
|
|
144
151
|
if (match !== undefined) return match
|
|
145
152
|
return item
|
|
146
153
|
}
|
|
@@ -148,72 +155,70 @@ export function manyOf<IsRequired extends boolean = false, T extends string | nu
|
|
|
148
155
|
return item
|
|
149
156
|
}
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const schema = baseSchema.superRefine((val, ctx) => {
|
|
167
|
-
if (val === null) {
|
|
168
|
-
if (isRequired) {
|
|
169
|
-
ctx.addIssue({ code: "custom", message: getMessage("required") })
|
|
158
|
+
// Build an array schema that validates + normalizes items, then applies superRefine for constraints
|
|
159
|
+
const arraySchema = z
|
|
160
|
+
.array(z.any())
|
|
161
|
+
.transform((arr) => arr.map(normalizeItem))
|
|
162
|
+
.superRefine((val, ctx) => {
|
|
163
|
+
// Check each item is in the allowed values
|
|
164
|
+
for (const item of val) {
|
|
165
|
+
if (!(valuesArr as readonly (string | number)[]).includes(item as string | number)) {
|
|
166
|
+
ctx.addIssue({
|
|
167
|
+
code: "custom",
|
|
168
|
+
message: getMessage("invalid", { values: valuesArr.join(", ") }),
|
|
169
|
+
})
|
|
170
|
+
return
|
|
171
|
+
}
|
|
170
172
|
}
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
// Duplicate check
|
|
175
|
+
if (!allowDuplicates) {
|
|
176
|
+
const seen = new Set()
|
|
177
|
+
for (const item of val) {
|
|
178
|
+
if (seen.has(item)) {
|
|
179
|
+
ctx.addIssue({ code: "custom", message: getMessage("duplicate") })
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
seen.add(item)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ctx.addIssue({
|
|
183
|
-
code: "custom",
|
|
184
|
-
message: getMessage("invalid", { values: values.join(", ") }),
|
|
185
|
-
})
|
|
186
|
+
// Min/max selection count
|
|
187
|
+
if (min !== undefined && val.length < min) {
|
|
188
|
+
ctx.addIssue({ code: "custom", message: getMessage("minSelect", { min }) })
|
|
186
189
|
return
|
|
187
190
|
}
|
|
188
|
-
}
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
for (const item of val) {
|
|
194
|
-
if (seen.has(item)) {
|
|
195
|
-
ctx.addIssue({ code: "custom", message: getMessage("duplicate") })
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
seen.add(item)
|
|
192
|
+
if (max !== undefined && val.length > max) {
|
|
193
|
+
ctx.addIssue({ code: "custom", message: getMessage("maxSelect", { max }) })
|
|
194
|
+
return
|
|
199
195
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
196
|
+
})
|
|
197
|
+
.transform((val) => {
|
|
198
|
+
if (!transform) return val as V[number][]
|
|
199
|
+
return transform(val as V[number][])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Single value → wrap in array
|
|
203
|
+
const singleToArray = z.union([z.string(), z.number()]).transform((v) => [normalizeItem(v)])
|
|
204
|
+
.pipe(arraySchema)
|
|
205
|
+
|
|
206
|
+
// Handle required vs optional
|
|
207
|
+
const fallback = defaultValue as V[number][] | null
|
|
208
|
+
|
|
209
|
+
if (isRequired && fallback === null) {
|
|
210
|
+
// Required, no default: empty values should fail with "required" message
|
|
211
|
+
const emptyRejectSchema = z
|
|
212
|
+
.union([z.literal("" as const), z.null(), z.undefined()])
|
|
213
|
+
.refine(() => false, { message: getMessage("required") })
|
|
214
|
+
|
|
215
|
+
return z.union([arraySchema, singleToArray, emptyRejectSchema]) as unknown as ManyOfSchema<IsRequired, V>
|
|
216
|
+
}
|
|
207
217
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
})
|
|
213
|
-
.transform((val) => {
|
|
214
|
-
if (val === null || !Array.isArray(val) || !transform) return val
|
|
215
|
-
return transform(val as T[])
|
|
216
|
-
})
|
|
218
|
+
// Optional or has default: empty values → fallback
|
|
219
|
+
const emptySchema = z
|
|
220
|
+
.union([z.literal("" as const), z.null(), z.undefined()])
|
|
221
|
+
.transform(() => fallback)
|
|
217
222
|
|
|
218
|
-
return
|
|
223
|
+
return z.union([arraySchema, singleToArray, emptySchema]) as unknown as ManyOfSchema<IsRequired, V>
|
|
219
224
|
}
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* Provides single-select validation that restricts input to a predefined set of allowed values,
|
|
5
5
|
* with support for case-insensitive matching, default values, and transformation.
|
|
6
6
|
*
|
|
7
|
+
* Uses z.enum() internally to preserve literal type inference for both z.input and z.output,
|
|
8
|
+
* making it compatible with React Hook Form resolvers and other type-aware form libraries.
|
|
9
|
+
*
|
|
7
10
|
* @author Ong Hoe Yuan
|
|
8
|
-
* @version 0.2.
|
|
11
|
+
* @version 0.2.5
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { z, ZodType } from "zod"
|
|
@@ -28,20 +31,20 @@ export type OneOfMessages = {
|
|
|
28
31
|
* Configuration options for oneOf validation
|
|
29
32
|
*
|
|
30
33
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
31
|
-
* @template
|
|
34
|
+
* @template V - The tuple type of allowed values, preserving literal types
|
|
32
35
|
*
|
|
33
36
|
* @interface OneOfOptions
|
|
34
|
-
* @property {
|
|
35
|
-
* @property {
|
|
37
|
+
* @property {V} values - Array of allowed values
|
|
38
|
+
* @property {V[number] | null} [defaultValue] - Default value when input is empty
|
|
36
39
|
* @property {boolean} [caseSensitive=true] - Whether string matching is case-sensitive
|
|
37
40
|
* @property {Function} [transform] - Custom transformation function applied after validation
|
|
38
41
|
* @property {Record<Locale, OneOfMessages>} [i18n] - Custom error messages for different locales
|
|
39
42
|
*/
|
|
40
|
-
export type OneOfOptions<IsRequired extends boolean = true,
|
|
41
|
-
values:
|
|
42
|
-
defaultValue?: IsRequired extends true ?
|
|
43
|
+
export type OneOfOptions<IsRequired extends boolean = true, V extends readonly (string | number)[] = readonly (string | number)[]> = {
|
|
44
|
+
values: V
|
|
45
|
+
defaultValue?: IsRequired extends true ? V[number] : V[number] | null
|
|
43
46
|
caseSensitive?: boolean
|
|
44
|
-
transform?: (value:
|
|
47
|
+
transform?: (value: V[number]) => V[number]
|
|
45
48
|
i18n?: Partial<Record<Locale, Partial<OneOfMessages>>>
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -49,18 +52,23 @@ export type OneOfOptions<IsRequired extends boolean = true, T extends string | n
|
|
|
49
52
|
* Type alias for oneOf validation schema based on required flag
|
|
50
53
|
*
|
|
51
54
|
* @template IsRequired - Whether the field is required
|
|
52
|
-
* @template
|
|
55
|
+
* @template V - The tuple type of allowed values
|
|
53
56
|
*/
|
|
54
|
-
export type OneOfSchema<IsRequired extends boolean,
|
|
57
|
+
export type OneOfSchema<IsRequired extends boolean, V extends readonly (string | number)[]> = IsRequired extends true
|
|
58
|
+
? ZodType<V[number], V[number] | "" | null | undefined>
|
|
59
|
+
: ZodType<V[number] | null, V[number] | "" | null | undefined>
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
62
|
* Creates a Zod schema for single-select validation that restricts values to a predefined set
|
|
58
63
|
*
|
|
64
|
+
* Uses z.enum() internally to preserve both z.input and z.output literal types,
|
|
65
|
+
* ensuring compatibility with React Hook Form and other type-aware form libraries.
|
|
66
|
+
*
|
|
59
67
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
60
|
-
* @template
|
|
68
|
+
* @template V - The tuple type of allowed values (inferred via const type parameter)
|
|
61
69
|
* @param {IsRequired} [required=false] - Whether the field is required
|
|
62
|
-
* @param {OneOfOptions<IsRequired,
|
|
63
|
-
* @returns {OneOfSchema<IsRequired,
|
|
70
|
+
* @param {OneOfOptions<IsRequired, V>} options - Configuration options (values is required)
|
|
71
|
+
* @returns {OneOfSchema<IsRequired, V>} Zod schema for oneOf validation
|
|
64
72
|
*
|
|
65
73
|
* @example
|
|
66
74
|
* ```typescript
|
|
@@ -69,7 +77,7 @@ export type OneOfSchema<IsRequired extends boolean, T> = IsRequired extends true
|
|
|
69
77
|
* roleSchema.parse("admin") // ✓ "admin"
|
|
70
78
|
* roleSchema.parse(null) // ✓ null
|
|
71
79
|
*
|
|
72
|
-
* // Required
|
|
80
|
+
* // Required — z.input and z.output are both "active" | "inactive" | "pending"
|
|
73
81
|
* const statusSchema = oneOf(true, { values: ["active", "inactive", "pending"] })
|
|
74
82
|
* statusSchema.parse("active") // ✓ "active"
|
|
75
83
|
* statusSchema.parse(null) // ✗ Required
|
|
@@ -103,13 +111,15 @@ export type OneOfSchema<IsRequired extends boolean, T> = IsRequired extends true
|
|
|
103
111
|
* sizeSchema.parse("m") // ✓ "M"
|
|
104
112
|
* ```
|
|
105
113
|
*/
|
|
106
|
-
export function oneOf<IsRequired extends boolean = false,
|
|
114
|
+
export function oneOf<IsRequired extends boolean = false, const V extends readonly (string | number)[] = readonly (string | number)[]>(
|
|
107
115
|
required?: IsRequired,
|
|
108
|
-
options?: Omit<OneOfOptions<IsRequired,
|
|
109
|
-
): OneOfSchema<IsRequired,
|
|
110
|
-
const { values = [] as unknown as
|
|
116
|
+
options?: Omit<OneOfOptions<IsRequired, V>, "required">,
|
|
117
|
+
): OneOfSchema<IsRequired, V> {
|
|
118
|
+
const { values = [] as unknown as V, defaultValue = null, caseSensitive = true, transform, i18n } = options ?? {}
|
|
111
119
|
|
|
112
120
|
const isRequired = required ?? (false as IsRequired)
|
|
121
|
+
const valuesArr = values as readonly (string | number)[]
|
|
122
|
+
const isAllStrings = valuesArr.every((v) => typeof v === "string")
|
|
113
123
|
|
|
114
124
|
const getMessage = (key: keyof OneOfMessages, params?: Record<string, any>) => {
|
|
115
125
|
if (i18n) {
|
|
@@ -123,50 +133,82 @@ export function oneOf<IsRequired extends boolean = false, T extends string | num
|
|
|
123
133
|
return t(`common.oneOf.${key}`, params)
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
const
|
|
127
|
-
if (val === "" || val === null || val === undefined) {
|
|
128
|
-
return defaultValue
|
|
129
|
-
}
|
|
136
|
+
const isEmpty = (v: unknown) => v === "" || v === null || v === undefined
|
|
130
137
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
// Build the value-matching schema using z.enum() or z.union(z.literal())
|
|
139
|
+
let valueSchema: ZodType
|
|
140
|
+
if (isAllStrings && valuesArr.length > 0) {
|
|
141
|
+
valueSchema = z.enum(valuesArr as unknown as readonly [string, ...string[]], {
|
|
142
|
+
error: (issue) => {
|
|
143
|
+
if (isEmpty(issue.input)) return getMessage("required")
|
|
144
|
+
return getMessage("invalid", { values: valuesArr.join(", ") })
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
} else if (valuesArr.length >= 2) {
|
|
148
|
+
const literals = valuesArr.map((v) => z.literal(v))
|
|
149
|
+
valueSchema = z.union(literals as unknown as readonly [ZodType, ZodType, ...ZodType[]], {
|
|
150
|
+
error: () => getMessage("invalid", { values: valuesArr.join(", ") }),
|
|
151
|
+
})
|
|
152
|
+
} else if (valuesArr.length === 1) {
|
|
153
|
+
valueSchema = z.literal(valuesArr[0])
|
|
154
|
+
} else {
|
|
155
|
+
valueSchema = z.never()
|
|
156
|
+
}
|
|
137
157
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
// Case-insensitive: pipe through a string normalizer
|
|
159
|
+
if (!caseSensitive) {
|
|
160
|
+
valueSchema = z
|
|
161
|
+
.string()
|
|
162
|
+
.transform((v) => {
|
|
163
|
+
const match = valuesArr.find((val) => typeof val === "string" && val.toLowerCase() === v.toLowerCase())
|
|
164
|
+
return (match ?? v) as string
|
|
165
|
+
})
|
|
166
|
+
.pipe(valueSchema as ZodType<any, any>)
|
|
167
|
+
}
|
|
144
168
|
|
|
145
|
-
|
|
169
|
+
// User transform (applied after validation)
|
|
170
|
+
if (transform) {
|
|
171
|
+
valueSchema = valueSchema.transform((v) => transform(v as V[number]))
|
|
146
172
|
}
|
|
147
173
|
|
|
148
|
-
|
|
174
|
+
// Handle required vs optional
|
|
175
|
+
const fallback = defaultValue as V[number] | null
|
|
176
|
+
const emptySchema = z
|
|
177
|
+
.union([z.literal("" as const), z.null(), z.undefined()])
|
|
178
|
+
.transform(() => fallback)
|
|
149
179
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
return
|
|
157
|
-
}
|
|
180
|
+
if (isRequired && fallback === null) {
|
|
181
|
+
// Required, no default: empty values should fail with "required" message
|
|
182
|
+
const emptyRejectSchema = z
|
|
183
|
+
.union([z.literal("" as const), z.null(), z.undefined()])
|
|
184
|
+
.refine(() => false, { message: getMessage("required") })
|
|
158
185
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
186
|
+
// For numeric values from forms (string "3" → number 3)
|
|
187
|
+
if (!isAllStrings) {
|
|
188
|
+
const numCoerceSchema = z
|
|
189
|
+
.string()
|
|
190
|
+
.transform((v) => {
|
|
191
|
+
const n = Number(v)
|
|
192
|
+
return isNaN(n) ? v : n
|
|
163
193
|
})
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
194
|
+
.pipe(valueSchema as ZodType<any, any>)
|
|
195
|
+
return z.union([valueSchema, numCoerceSchema, emptyRejectSchema]) as unknown as OneOfSchema<IsRequired, V>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return z.union([valueSchema, emptyRejectSchema]) as unknown as OneOfSchema<IsRequired, V>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Required with default or optional: empty values → fallback
|
|
202
|
+
if (!isAllStrings) {
|
|
203
|
+
const numCoerceSchema = z
|
|
204
|
+
.string()
|
|
205
|
+
.transform((v) => {
|
|
206
|
+
const n = Number(v)
|
|
207
|
+
return isNaN(n) ? v : n
|
|
208
|
+
})
|
|
209
|
+
.pipe(valueSchema as ZodType<any, any>)
|
|
210
|
+
return z.union([valueSchema, numCoerceSchema, emptySchema]) as unknown as OneOfSchema<IsRequired, V>
|
|
211
|
+
}
|
|
170
212
|
|
|
171
|
-
return
|
|
213
|
+
return z.union([valueSchema, emptySchema]) as unknown as OneOfSchema<IsRequired, V>
|
|
172
214
|
}
|