Add predicate factory mode to is.any and is.all

Fixes #218
This commit is contained in:
Sindre Sorhus 2025-12-20 02:51:20 +01:00
parent fbcc68e139
commit 9bdcd9b57f
3 changed files with 245 additions and 16 deletions

View file

@ -575,6 +575,31 @@ is.any([is.boolean, is.number], 'unicorns', [], new Map());
//=> false
```
##### .any(predicate[])
Using an array of `predicate[]` without values, returns a combined type guard that checks if a value matches **any** of the predicates:
```js
const isStringOrNumber = is.any([is.string, is.number]);
isStringOrNumber('hello');
//=> true
isStringOrNumber(123);
//=> true
isStringOrNumber(true);
//=> false
```
This is useful for composing with other methods like `is.optional`:
```js
is.optional(value, is.any([is.string, is.number]));
```
An empty predicate array currently returns a predicate that always returns `false`. This will throw in the next major release.
##### .all(predicate, ...values)
Returns `true` if **all** of the input `values` returns true in the `predicate`:
@ -587,6 +612,28 @@ is.all(is.string, '🦄', [], 'unicorns');
//=> false
```
##### .all(predicate[])
Using an array of `predicate[]` without values, returns a combined type guard that checks if a value matches **all** of the predicates:
```js
const isArrayAndNonEmpty = is.all([is.array, is.nonEmptyArray]);
isArrayAndNonEmpty(['hello']);
//=> true
isArrayAndNonEmpty([]);
//=> false
```
This is useful for composing with other methods like `is.optional`:
```js
is.optional(value, is.all([is.object, is.plainObject]));
```
An empty predicate array currently returns a predicate that always returns `true`. This will throw in the next major release.
##### .optional(value, predicate)
Returns `true` if `value` is `undefined` or satisfies the given `predicate`.

View file

@ -332,15 +332,77 @@ function isAbsoluteModule2(remainder: 0 | 1) {
return (value: unknown): value is number => isInteger(value) && Math.abs(value % 2) === remainder;
}
export function isAll(predicate: Predicate, ...values: unknown[]): boolean {
return predicateOnArray(Array.prototype.every, predicate, values);
type TypeGuard<T> = (value: unknown) => value is T;
function validatePredicateArray(predicateArray: readonly Predicate[], allowEmpty: boolean) {
if (predicateArray.length === 0) {
if (allowEmpty) {
// Next major release: throw for empty predicate arrays to avoid vacuous results.
// throw new TypeError('Invalid predicate array');
} else {
throw new TypeError('Invalid predicate array');
}
return;
}
for (const predicate of predicateArray) {
if (!isFunction(predicate)) {
throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`);
}
}
}
export function isAny(predicate: Predicate | Predicate[], ...values: unknown[]): boolean {
const predicates = isArray(predicate) ? predicate : [predicate];
return predicates.some(singlePredicate =>
predicateOnArray(Array.prototype.some, singlePredicate, values),
);
// Predicate factory overloads - return a type guard when called with only predicates
export function isAll<T1>(predicates: [TypeGuard<T1>]): TypeGuard<T1>;
export function isAll<T1, T2>(predicates: [TypeGuard<T1>, TypeGuard<T2>]): TypeGuard<T1 & T2>;
export function isAll<T1, T2, T3>(predicates: [TypeGuard<T1>, TypeGuard<T2>, TypeGuard<T3>]): TypeGuard<T1 & T2 & T3>;
export function isAll<T1, T2, T3, T4>(predicates: [TypeGuard<T1>, TypeGuard<T2>, TypeGuard<T3>, TypeGuard<T4>]): TypeGuard<T1 & T2 & T3 & T4>;
export function isAll<T1, T2, T3, T4, T5>(predicates: [TypeGuard<T1>, TypeGuard<T2>, TypeGuard<T3>, TypeGuard<T4>, TypeGuard<T5>]): TypeGuard<T1 & T2 & T3 & T4 & T5>;
export function isAll(predicates: ReadonlyArray<TypeGuard<unknown>>): TypeGuard<unknown>;
export function isAll(predicates: readonly Predicate[]): Predicate;
// Evaluator overload - check if all values match the predicate
export function isAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean;
export function isAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean | Predicate {
if (Array.isArray(predicate)) {
const predicateArray = predicate as readonly Predicate[];
validatePredicateArray(predicateArray, values.length === 0);
const combinedPredicate = (value: unknown) => predicateArray.every(singlePredicate => singlePredicate(value));
if (values.length === 0) {
return combinedPredicate;
}
return predicateOnArray(Array.prototype.every, combinedPredicate, values);
}
return predicateOnArray(Array.prototype.every, predicate as Predicate, values);
}
// Predicate factory overloads - return a type guard when called with only predicates
export function isAny<T1>(predicates: [TypeGuard<T1>]): TypeGuard<T1>;
export function isAny<T1, T2>(predicates: [TypeGuard<T1>, TypeGuard<T2>]): TypeGuard<T1 | T2>;
export function isAny<T1, T2, T3>(predicates: [TypeGuard<T1>, TypeGuard<T2>, TypeGuard<T3>]): TypeGuard<T1 | T2 | T3>;
export function isAny<T1, T2, T3, T4>(predicates: [TypeGuard<T1>, TypeGuard<T2>, TypeGuard<T3>, TypeGuard<T4>]): TypeGuard<T1 | T2 | T3 | T4>;
export function isAny<T1, T2, T3, T4, T5>(predicates: [TypeGuard<T1>, TypeGuard<T2>, TypeGuard<T3>, TypeGuard<T4>, TypeGuard<T5>]): TypeGuard<T1 | T2 | T3 | T4 | T5>;
export function isAny(predicates: ReadonlyArray<TypeGuard<unknown>>): TypeGuard<unknown>;
export function isAny(predicates: readonly Predicate[]): Predicate;
// Evaluator overload - check if any value matches any predicate
export function isAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean;
export function isAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean | Predicate {
if (Array.isArray(predicate)) {
const predicateArray = predicate as readonly Predicate[];
validatePredicateArray(predicateArray, values.length === 0);
const combinedPredicate = (value: unknown) => predicateArray.some(singlePredicate => singlePredicate(value));
if (values.length === 0) {
return combinedPredicate;
}
return predicateOnArray(Array.prototype.some, combinedPredicate, values);
}
return predicateOnArray(Array.prototype.some, predicate as Predicate, values);
}
export function isOptional<T>(value: unknown, predicate: (value: unknown) => value is T): value is T | undefined {
@ -715,8 +777,6 @@ export function isTruthy<T>(value: T | Falsy): value is T {
return Boolean(value);
}
type TypeGuard<T> = (value: unknown) => value is T;
// eslint-disable-next-line @typescript-eslint/ban-types
type ResolveTypesOfTypeGuardsTuple<TypeGuardsOfT, ResultOfT extends unknown[] = [] > =
TypeGuardsOfT extends [TypeGuard<infer U>, ...infer TOthers]
@ -943,8 +1003,8 @@ type Assert = {
inRange: (value: number, range: number | [number, number], message?: string) => asserts value is number;
// Variadic functions.
any: (predicate: Predicate | Predicate[], ...values: unknown[]) => void | never;
all: (predicate: Predicate, ...values: unknown[]) => void | never;
any: (predicate: Predicate | readonly Predicate[], ...values: unknown[]) => void | never;
all: (predicate: Predicate | readonly Predicate[], ...values: unknown[]) => void | never;
/**
Asserts that `value` is `undefined` or satisfies the provided `assertion`.
@ -1146,17 +1206,26 @@ function isIsMethodName(value: unknown): value is IsMethodName {
return isMethodNames.includes(value as IsMethodName);
}
export function assertAll(predicate: Predicate, ...values: unknown[]): void | never {
export function assertAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): void | never {
if (values.length === 0) {
throw new TypeError('Invalid number of values');
}
if (!isAll(predicate, ...values)) {
const expectedType = isIsMethodName(predicate.name) ? methodTypeMap[predicate.name] : 'predicate returns truthy for all values';
const predicateFunction = predicate as Predicate;
const expectedType = !Array.isArray(predicate) && isIsMethodName(predicateFunction.name) ? methodTypeMap[predicateFunction.name] : 'predicate returns truthy for all values';
throw new TypeError(typeErrorMessageMultipleValues(expectedType, values));
}
}
export function assertAny(predicate: Predicate | Predicate[], ...values: unknown[]): void | never {
export function assertAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): void | never {
if (values.length === 0) {
throw new TypeError('Invalid number of values');
}
if (!isAny(predicate, ...values)) {
const predicates = isArray(predicate) ? predicate : [predicate];
const expectedTypes = predicates.map(predicate => isIsMethodName(predicate.name) ? methodTypeMap[predicate.name] : 'predicate returns truthy for any value');
const predicates = Array.isArray(predicate) ? predicate as readonly Predicate[] : [predicate as Predicate];
const expectedTypes = predicates.map(singlePredicate => isIsMethodName(singlePredicate.name) ? methodTypeMap[singlePredicate.name] : 'predicate returns truthy for any value');
throw new TypeError(typeErrorMessageMultipleValues(expectedTypes, values));
}
}

View file

@ -1648,12 +1648,17 @@ test('is.any', t => {
t.false(is.any(is.integer, true, 'lol', {}));
t.true(is.any([is.string, is.number], {}, true, '🦄'));
t.false(is.any([is.boolean, is.number], 'unicorns', [], new Map()));
t.is(typeof is.any([is.string, is.number]), 'function');
t.throws(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
is.any(null as any, true);
});
t.throws(() => {
is.any([], 'value');
});
t.throws(() => {
is.any(is.string);
});
@ -1666,6 +1671,10 @@ test('is.any', t => {
assert.any(is.object, false, {}, 'unicorns');
});
t.throws(() => {
assert.any([is.string, is.number]);
});
t.throws(() => {
assert.any(is.boolean, '🦄', [], 3);
});
@ -1679,6 +1688,10 @@ test('is.any', t => {
assert.any(null as any, true);
});
t.throws(() => {
assert.any([], 'value');
});
t.throws(() => {
assert.any(is.string);
});
@ -1726,12 +1739,18 @@ test('is.all', t => {
t.false(is.all(is.set, new Map(), {}));
t.true(is.all(is.array, ['1'], ['2']));
t.true(is.all([is.string, is.nonEmptyString], '🦄', 'unicorns'));
t.false(is.all([is.string, is.number], '🦄'));
t.throws(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
is.all(null as any, true);
});
t.throws(() => {
is.all([], 'value');
});
t.throws(() => {
is.all(is.string);
});
@ -1744,10 +1763,22 @@ test('is.all', t => {
assert.all(is.boolean, true, false);
});
t.throws(() => {
assert.all([is.string, is.number]);
});
t.notThrows(() => {
assert.all([is.string, is.nonEmptyString], '🦄', 'unicorns');
});
t.throws(() => {
assert.all(is.string, '🦄', []);
});
t.throws(() => {
assert.all([is.string, is.number], '🦄');
});
t.throws(() => {
assert.all(is.set, new Map(), {});
});
@ -1757,6 +1788,10 @@ test('is.all', t => {
assert.all(null as any, true);
});
t.throws(() => {
assert.all([], 'value');
});
t.throws(() => {
assert.all(is.string);
});
@ -1783,6 +1818,84 @@ test('is.all', t => {
});
});
test('is.any as predicate factory', t => {
// Returns a type guard function when called with only predicates
const isStringOrNumber = is.any([is.string, is.number]);
t.is(typeof isStringOrNumber, 'function');
t.true(isStringOrNumber('hello'));
t.true(isStringOrNumber(123));
t.false(isStringOrNumber(true));
t.false(isStringOrNumber({}));
// Type narrowing works correctly
const value: unknown = 'test';
if (isStringOrNumber(value)) {
// TypeScript should narrow to string | number
const narrowed: string | number = value;
t.pass(`narrowed to: ${typeof narrowed}`);
}
// Works with is.optional
t.true(is.optional(undefined, is.any([is.string, is.number])));
t.true(is.optional('test', is.any([is.string, is.number])));
t.true(is.optional(42, is.any([is.string, is.number])));
t.false(is.optional(true, is.any([is.string, is.number])));
const predicateArray: Predicate[] = [is.string, is.number];
const isStringOrNumberFromArray = is.any(predicateArray);
t.is(typeof isStringOrNumberFromArray, 'function');
t.true(isStringOrNumberFromArray('hello'));
t.true(isStringOrNumberFromArray(123));
t.false(isStringOrNumberFromArray(true));
// Type narrowing with is.optional
const optionalValue: unknown = undefined;
if (is.optional(optionalValue, is.any([is.string, is.number]))) {
// TypeScript should narrow to string | number | undefined
const narrowed: string | number | undefined = optionalValue;
t.pass(`optional narrowed to: ${typeof narrowed}`);
}
// Works with more predicates
const isStringOrNumberOrBoolean = is.any([is.string, is.number, is.boolean]);
t.true(isStringOrNumberOrBoolean('hello'));
t.true(isStringOrNumberOrBoolean(123));
t.true(isStringOrNumberOrBoolean(true));
t.false(isStringOrNumberOrBoolean({}));
t.throws(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
is.any([is.string, 123 as any]);
});
});
test('is.all as predicate factory', t => {
// Returns a type guard function when called with only predicates
const isArrayAndNonEmpty = is.all([is.array, is.nonEmptyArray]);
t.is(typeof isArrayAndNonEmpty, 'function');
t.true(isArrayAndNonEmpty(['hello']));
t.false(isArrayAndNonEmpty([]));
t.false(isArrayAndNonEmpty('hello'));
// Type narrowing works correctly
const value: unknown = ['test'];
if (isArrayAndNonEmpty(value)) {
// TypeScript should narrow to the intersection type
t.true(Array.isArray(value));
t.true(value.length > 0);
}
// Works with is.optional
t.true(is.optional(undefined, is.all([is.object, is.plainObject])));
t.true(is.optional({foo: 'bar'}, is.all([is.object, is.plainObject])));
t.false(is.optional([], is.all([is.object, is.plainObject])));
t.throws(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
is.all([is.string, 123 as any]);
});
});
test('is.formData supplemental', t => {
const data = new window.FormData();
t.true(is.formData(data));