Skip to content

Commit 445107e

Browse files
committed
Validate numbers, objects, and undefinables in the json module
This commit adds a `number` validator`, an `object` validator, an `isNumber` predicate, and `undefinable()` to test optional-but-not-null properties.
1 parent 6010f85 commit 445107e

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

src/json/index.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,26 @@ test("validateSchema - optional properties are optional", async (t) => {
4444
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
4545
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
4646
});
47+
48+
const undefinableSchema = {
49+
optionalKey: json.undefinable(json.string),
50+
};
51+
52+
test("validateSchema - undefinable properties are optional but reject null", async (t) => {
53+
// Optional fields may be absent or explicitly undefined
54+
t.true(json.validateSchema(undefinableSchema, {}));
55+
t.true(json.validateSchema(undefinableSchema, { optionalKey: undefined }));
56+
57+
// But, unlike `optional`, `null` is rejected
58+
t.false(json.validateSchema(undefinableSchema, { optionalKey: null }));
59+
60+
// And, if present, should have the expected type
61+
t.false(json.validateSchema(undefinableSchema, { optionalKey: 0 }));
62+
t.false(json.validateSchema(undefinableSchema, { optionalKey: 123 }));
63+
t.false(json.validateSchema(undefinableSchema, { optionalKey: false }));
64+
t.false(json.validateSchema(undefinableSchema, { optionalKey: true }));
65+
t.false(json.validateSchema(undefinableSchema, { optionalKey: [] }));
66+
t.false(json.validateSchema(undefinableSchema, { optionalKey: {} }));
67+
t.true(json.validateSchema(undefinableSchema, { optionalKey: "" }));
68+
t.true(json.validateSchema(undefinableSchema, { optionalKey: "foo" }));
69+
});

src/json/index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export function isString(value: unknown): value is string {
3030
return typeof value === "string";
3131
}
3232

33+
/** Asserts that `value` is a number. */
34+
export function isNumber(value: unknown): value is number {
35+
return typeof value === "number";
36+
}
37+
3338
/** Asserts that `value` is either a string or undefined. */
3439
export function isStringOrUndefined(
3540
value: unknown,
@@ -55,7 +60,22 @@ export const string = {
5560
required: true,
5661
} as const satisfies Validator<string>;
5762

58-
/** Transforms a validator to be optional. */
63+
/** A validator for number fields in schemas. */
64+
export const number = {
65+
validate: isNumber,
66+
required: true,
67+
} as const satisfies Validator<number>;
68+
69+
/** A validator for object fields in schemas. */
70+
export const object = {
71+
validate: isObject,
72+
required: true,
73+
} as const satisfies Validator<UnvalidatedObject<unknown>>;
74+
75+
/**
76+
* Transforms a validator to be optional, accepting `undefined` or `null` for an
77+
* absent value.
78+
*/
5979
export function optional<T>(validator: Validator<T>) {
6080
return {
6181
validate: (val: unknown) => {
@@ -65,6 +85,19 @@ export function optional<T>(validator: Validator<T>) {
6585
} as const satisfies Validator<T | undefined | null>;
6686
}
6787

88+
/**
89+
* Transforms a validator to be optional, accepting `undefined` for an absent
90+
* value but, unlike `optional`, rejecting `null`.
91+
*/
92+
export function undefinable<T>(validator: Validator<T>) {
93+
return {
94+
validate: (val: unknown): val is T | undefined => {
95+
return val === undefined || validator.validate(val);
96+
},
97+
required: false,
98+
} as const satisfies Validator<T | undefined>;
99+
}
100+
68101
/** Represents an arbitrary object schema. */
69102
export type Schema = Record<string, Validator<any>>;
70103

0 commit comments

Comments
 (0)