From 1f2440ae0d964721d1eb87aa2c334a995757dd12 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 13 Sep 2025 02:27:50 +0700 Subject: [PATCH] Add `is.optional` and `assert.optional` Fixes #111 --- readme.md | 32 ++++++++++++++++++++++++++++++++ source/index.ts | 19 +++++++++++++++++++ test/test.ts | 25 +++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/readme.md b/readme.md index 1966a5b..9a4982c 100644 --- a/readme.md +++ b/readme.md @@ -587,6 +587,21 @@ is.all(is.string, '🦄', [], 'unicorns'); //=> 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) Returns `true` if the value is a valid date. @@ -682,6 +697,23 @@ handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'}); 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 The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise` and `Map`. 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. diff --git a/source/index.ts b/source/index.ts index b89c90e..f40fad0 100644 --- a/source/index.ts +++ b/source/index.ts @@ -318,6 +318,7 @@ const is = Object.assign( urlInstance: isUrlInstance, urlSearchParams: isUrlSearchParams, urlString: isUrlString, + optional: isOptional, validDate: isValidDate, validLength: isValidLength, weakMap: isWeakMap, @@ -342,6 +343,10 @@ export function isAny(predicate: Predicate | Predicate[], ...values: unknown[]): ); } +export function isOptional(value: unknown, predicate: (value: unknown) => value is T): value is T | undefined { + return isUndefined(value) || predicate(value); +} + export function isArray(value: unknown, assertion?: (value: T) => value is T): value is T[] { if (!Array.isArray(value)) { return false; @@ -940,11 +945,19 @@ type Assert = { // Variadic functions. any: (predicate: 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: (value: unknown, assertion: (value: unknown, message?: string) => asserts value is T, message?: string) => asserts value is T | undefined; }; export const assert: Assert = { all: assertAll, any: assertAny, + optional: assertOptional, array: assertArray, arrayBuffer: assertArrayBuffer, arrayLike: assertArrayLike, @@ -1148,6 +1161,12 @@ export function assertAny(predicate: Predicate | Predicate[], ...values: unknown } } +export function assertOptional(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(value: unknown, assertion?: (element: unknown, message?: string) => asserts element is T, message?: string): asserts value is T[] { if (!isArray(value)) { throw new TypeError(message ?? typeErrorMessage('Array', value)); diff --git a/test/test.ts b/test/test.ts index 32e6c1a..523bf11 100644 --- a/test/test.ts +++ b/test/test.ts @@ -2251,3 +2251,28 @@ test('custom assertion message', t => { assert.whitespaceString(undefined, 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); + }); +});