Add is.optional and assert.optional

Fixes #111
This commit is contained in:
Sindre Sorhus 2025-09-13 02:27:50 +07:00
parent c68ad76062
commit 1f2440ae0d
3 changed files with 76 additions and 0 deletions

View file

@ -587,6 +587,21 @@ is.all(is.string, '🦄', [], 'unicorns');
//=> false //=> false
``` ```
##### .optional(value, predicate)
Returns `true` if `value` is `undefined` or satisfies the given `predicate`.
```js
is.optional(undefined, is.string);
//=> true
is.optional('🦄', is.string);
//=> true
is.optional(123, is.string);
//=> false
```
##### .validDate(value) ##### .validDate(value)
Returns `true` if the value is a valid date. Returns `true` if the value is a valid date.
@ -682,6 +697,23 @@ handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'});
handleMovieRatingApiResponse({rating: '🦄'}); handleMovieRatingApiResponse({rating: '🦄'});
``` ```
### Optional assertion
Asserts that `value` is `undefined` or satisfies the provided `assertion`.
```ts
import {assert} from '@sindresorhus/is';
assert.optional(undefined, assert.string);
// Passes without throwing
assert.optional('🦄', assert.string);
// Passes without throwing
assert.optional(123, assert.string);
// Throws: Expected value which is `string`, received value of type `number`
```
## Generic type parameters ## Generic type parameters
The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise<T>` and `Map<Key, Value>`. The default is `unknown` for most cases, since `is` cannot check them at runtime. If the generic type is known at compile-time, either implicitly (inferred) or explicitly (provided), `is` propagates the type so it can be used later. The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise<T>` and `Map<Key, Value>`. The default is `unknown` for most cases, since `is` cannot check them at runtime. If the generic type is known at compile-time, either implicitly (inferred) or explicitly (provided), `is` propagates the type so it can be used later.

View file

@ -318,6 +318,7 @@ const is = Object.assign(
urlInstance: isUrlInstance, urlInstance: isUrlInstance,
urlSearchParams: isUrlSearchParams, urlSearchParams: isUrlSearchParams,
urlString: isUrlString, urlString: isUrlString,
optional: isOptional,
validDate: isValidDate, validDate: isValidDate,
validLength: isValidLength, validLength: isValidLength,
weakMap: isWeakMap, weakMap: isWeakMap,
@ -342,6 +343,10 @@ export function isAny(predicate: Predicate | Predicate[], ...values: unknown[]):
); );
} }
export function isOptional<T>(value: unknown, predicate: (value: unknown) => value is T): value is T | undefined {
return isUndefined(value) || predicate(value);
}
export function isArray<T = unknown>(value: unknown, assertion?: (value: T) => value is T): value is T[] { export function isArray<T = unknown>(value: unknown, assertion?: (value: T) => value is T): value is T[] {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return false; return false;
@ -940,11 +945,19 @@ type Assert = {
// Variadic functions. // Variadic functions.
any: (predicate: Predicate | Predicate[], ...values: unknown[]) => void | never; any: (predicate: Predicate | Predicate[], ...values: unknown[]) => void | never;
all: (predicate: Predicate, ...values: unknown[]) => void | never; all: (predicate: Predicate, ...values: unknown[]) => void | never;
/**
Asserts that `value` is `undefined` or satisfies the provided `assertion`.
Useful for optional inputs.
*/
optional: <T>(value: unknown, assertion: (value: unknown, message?: string) => asserts value is T, message?: string) => asserts value is T | undefined;
}; };
export const assert: Assert = { export const assert: Assert = {
all: assertAll, all: assertAll,
any: assertAny, any: assertAny,
optional: assertOptional,
array: assertArray, array: assertArray,
arrayBuffer: assertArrayBuffer, arrayBuffer: assertArrayBuffer,
arrayLike: assertArrayLike, arrayLike: assertArrayLike,
@ -1148,6 +1161,12 @@ export function assertAny(predicate: Predicate | Predicate[], ...values: unknown
} }
} }
export function assertOptional<T>(value: unknown, assertion: (value: unknown, message?: string) => asserts value is T, message?: string): asserts value is T | undefined {
if (!isUndefined(value)) {
assertion(value, message);
}
}
export function assertArray<T = unknown>(value: unknown, assertion?: (element: unknown, message?: string) => asserts element is T, message?: string): asserts value is T[] { export function assertArray<T = unknown>(value: unknown, assertion?: (element: unknown, message?: string) => asserts element is T, message?: string): asserts value is T[] {
if (!isArray(value)) { if (!isArray(value)) {
throw new TypeError(message ?? typeErrorMessage('Array', value)); throw new TypeError(message ?? typeErrorMessage('Array', value));

View file

@ -2251,3 +2251,28 @@ test('custom assertion message', t => {
assert.whitespaceString(undefined, message); assert.whitespaceString(undefined, message);
}, {instanceOf: TypeError, message}); }, {instanceOf: TypeError, message});
}); });
test('is.optional', t => {
t.true(is.optional(undefined, is.string));
t.true(is.optional('🦄', is.string));
t.false(is.optional(123, is.string));
t.false(is.optional(null, is.string));
});
test('assert.optional', t => {
t.notThrows(() => {
assert.optional(undefined, assert.string);
});
t.notThrows(() => {
assert.optional('🦄', assert.string);
});
t.throws(() => {
assert.optional(123, assert.string);
});
t.throws(() => {
assert.optional(null, assert.string);
});
});