From 3868f47783636086b37aae56a5a715b6639e6714 Mon Sep 17 00:00:00 2001 From: Tal Michel Date: Sun, 23 Jul 2023 15:35:23 +0300 Subject: [PATCH] Add `.tupleLike()` (#189) Co-authored-by: Sindre Sorhus --- package.json | 3 +- readme.md | 20 +++++++++++++ source/index.ts | 21 ++++++++++++++ test/test.ts | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e13525..5a41f33 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "ts-node": "^10.9.1", "typescript": "^5.0.4", "xo": "^0.54.2", - "zen-observable": "^0.10.0" + "zen-observable": "^0.10.0", + "expect-type": "^0.16.0" }, "sideEffects": false, "ava": { diff --git a/readme.md b/readme.md index fc451a3..40726c5 100644 --- a/readme.md +++ b/readme.md @@ -410,6 +410,26 @@ function foo() { foo(); ``` +##### .tupleLike(value, guards) + +A `value` is tuple-like if it matches the provided `guards` array both in `.length` and in types. + +```js +is.tupleLike([1], [is.number]); +//=> true +``` + +```js +function foo() { + const tuple = [1, '2', true]; + if (is.tupleLike(tuple, [is.number, is.string, is.boolean])) { + tuple // [number, string, boolean] + } +} + +foo(); +``` + #### .positiveNumber(value) Check if `value` is a number and is more than 0. diff --git a/source/index.ts b/source/index.ts index d69b9ce..122917b 100644 --- a/source/index.ts +++ b/source/index.ts @@ -326,6 +326,24 @@ export type ArrayLike = { const isValidLength = (value: unknown): value is number => is.safeInteger(value) && value >= 0; is.arrayLike = (value: unknown): value is ArrayLike => !is.nullOrUndefined(value) && !is.function_(value) && isValidLength((value as ArrayLike).length); +type TypeGuard = (value: unknown) => value is T; + +// eslint-disable-next-line @typescript-eslint/ban-types +type ResolveTypesOfTypeGuardsTuple = + TypeGuardsOfT extends [TypeGuard, ...infer TOthers] + ? ResolveTypesOfTypeGuardsTuple + : TypeGuardsOfT extends undefined[] + ? ResultOfT + : never; + +is.tupleLike = >>(value: unknown, guards: [...T]): value is ResolveTypesOfTypeGuardsTuple => { + if (is.array(guards) && is.array(value) && guards.length === value.length) { + return guards.every((guard, index) => guard(value[index])); + } + + return false; +}; + is.inRange = (value: number, range: number | number[]): value is number => { if (is.number(range)) { return value >= Math.min(0, range) && value <= Math.max(range, 0); @@ -482,6 +500,7 @@ export const enum AssertionTypeDescription { safeInteger = 'integer', // eslint-disable-line @typescript-eslint/no-duplicate-enum-values plainObject = 'plain object', arrayLike = 'array-like', + tupleLike = 'tuple-like', typedArray = 'TypedArray', domElement = 'HTMLElement', nodeStream = 'Node.js Stream', @@ -579,6 +598,7 @@ type Assert = { plainObject: (value: unknown) => asserts value is Record; typedArray: (value: unknown) => asserts value is TypedArray; arrayLike: (value: unknown) => asserts value is ArrayLike; + tupleLike: >>(value: unknown, guards: [...T]) => asserts value is ResolveTypesOfTypeGuardsTuple; domElement: (value: unknown) => asserts value is HTMLElement; observable: (value: unknown) => asserts value is ObservableLike; nodeStream: (value: unknown) => asserts value is NodeStream; @@ -687,6 +707,7 @@ export const assert: Assert = { plainObject: (value: unknown): asserts value is Record => assertType(is.plainObject(value), AssertionTypeDescription.plainObject, value), typedArray: (value: unknown): asserts value is TypedArray => assertType(is.typedArray(value), AssertionTypeDescription.typedArray, value), arrayLike: (value: unknown): asserts value is ArrayLike => assertType(is.arrayLike(value), AssertionTypeDescription.arrayLike, value), + tupleLike: >>(value: unknown, guards: [...T]): asserts value is ResolveTypesOfTypeGuardsTuple => assertType(is.tupleLike(value, guards), AssertionTypeDescription.tupleLike, value), domElement: (value: unknown): asserts value is HTMLElement => assertType(is.domElement(value), AssertionTypeDescription.domElement, value), observable: (value: unknown): asserts value is ObservableLike => assertType(is.observable(value), 'Observable', value), nodeStream: (value: unknown): asserts value is NodeStream => assertType(is.nodeStream(value), AssertionTypeDescription.nodeStream, value), diff --git a/test/test.ts b/test/test.ts index a593332..7bd1aed 100644 --- a/test/test.ts +++ b/test/test.ts @@ -8,6 +8,7 @@ import test, {type ExecutionContext} from 'ava'; import {JSDOM} from 'jsdom'; import {Subject, Observable} from 'rxjs'; import {temporaryFile} from 'tempy'; +import {expectTypeOf} from 'expect-type'; import ZenObservable from 'zen-observable'; import is, { assert, @@ -1445,6 +1446,81 @@ test('is.arrayLike', t => { }); }); +test('is.tupleLike', t => { + (function () { + t.false(is.tupleLike(arguments, [])); // eslint-disable-line prefer-rest-params + })(); + + t.true(is.tupleLike([], [])); + t.true(is.tupleLike([1, '2', true, {}, [], undefined, null], [is.number, is.string, is.boolean, is.object, is.array, is.undefined, is.nullOrUndefined])); + t.false(is.tupleLike('unicorn', [is.string])); + + t.false(is.tupleLike({}, [])); + t.false(is.tupleLike(() => {}, [is.function_])); + t.false(is.tupleLike(new Map(), [is.map])); + + (function () { + t.throws(function () { + assert.tupleLike(arguments, []); // eslint-disable-line prefer-rest-params + }); + })(); + + t.notThrows(() => { + assert.tupleLike([], []); + }); + t.throws(() => { + assert.tupleLike('unicorn', [is.string]); + }); + + t.throws(() => { + assert.tupleLike({}, [is.object]); + }); + t.throws(() => { + assert.tupleLike(() => {}, [is.function_]); + }); + t.throws(() => { + assert.tupleLike(new Map(), [is.map]); + }); + + { + const tuple = [[false, 'unicorn'], 'string', true]; + + if (is.tupleLike(tuple, [is.array, is.string, is.boolean])) { + if (is.tupleLike(tuple[0], [is.boolean, is.string])) { // eslint-disable-line unicorn/no-lonely-if + const value = tuple[0][1]; + expectTypeOf(value).toEqualTypeOf(); + } + } + } + + { + const tuple = [{isTest: true}, '1', true, null]; + + if (is.tupleLike(tuple, [is.nonEmptyObject, is.string, is.boolean, is.null_])) { + const value = tuple[0]; + expectTypeOf(value).toEqualTypeOf>(); + } + } + + { + const tuple = [1, '1', true, null, undefined]; + + if (is.tupleLike(tuple, [is.number, is.string, is.boolean, is.undefined, is.null_])) { + const numericValue = tuple[0]; + const stringValue = tuple[1]; + const booleanValue = tuple[2]; + const undefinedValue = tuple[3]; + const nullValue = tuple[4]; + expectTypeOf(numericValue).toEqualTypeOf(); + expectTypeOf(stringValue).toEqualTypeOf(); + expectTypeOf(booleanValue).toEqualTypeOf(); + expectTypeOf(undefinedValue).toEqualTypeOf(); + // eslint-disable-next-line @typescript-eslint/ban-types + expectTypeOf(nullValue).toEqualTypeOf(); + } + } +}); + test('is.inRange', t => { const x = 3;