diff --git a/__snapshots__/is_test.ts.snap b/__snapshots__/is_test.ts.snap index 4355972..9da812f 100644 --- a/__snapshots__/is_test.ts.snap +++ b/__snapshots__/is_test.ts.snap @@ -70,6 +70,54 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; +snapshot[`isParametersOf > returns properly named function 1`] = ` +"isParametersOf([ + isNumber, + isString, + isOptionalOf(isBoolean) +])" +`; + +snapshot[`isParametersOf > returns properly named function 2`] = `"isParametersOf([(anonymous)])"`; + +snapshot[`isParametersOf > returns properly named function 3`] = `"isParametersOf([])"`; + +snapshot[`isParametersOf > returns properly named function 4`] = ` +"isParametersOf([ + isParametersOf([ + isParametersOf([ + isNumber, + isString, + isOptionalOf(isBoolean) + ]) + ]) +])" +`; + +snapshot[`isParametersOf > returns properly named function 1`] = ` +"isParametersOf([ + isNumber, + isString, + isOptionalOf(isBoolean) +], isArray)" +`; + +snapshot[`isParametersOf > returns properly named function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`; + +snapshot[`isParametersOf > returns properly named function 3`] = `"isParametersOf([], isArrayOf(isString))"`; + +snapshot[`isParametersOf > returns properly named function 4`] = ` +"isParametersOf([ + isParametersOf([ + isParametersOf([ + isNumber, + isString, + isOptionalOf(isBoolean) + ], isArray) + ], isArray) +])" +`; + snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyOf(isTupleOf([ isNumber, diff --git a/is.ts b/is.ts index 58e9b7a..1bf7d66 100644 --- a/is.ts +++ b/is.ts @@ -751,6 +751,138 @@ type IsTupleOfMetadata = { args: [Parameters[0], Parameters[1]?]; }; +/** + * Return a type predicate function that returns `true` if the type of `x` is `ParametersOf` or `ParametersOf`. + * + * This is similar to `TupleOf` or `TupleOf`, but if `is.OptionalOf()` is specified at the trailing, the trailing elements becomes optional and makes variable-length tuple. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ParametersOf([ + * is.Number, + * is.OptionalOf(is.String), + * is.Boolean, + * is.OptionalOf(is.Number), + * is.OptionalOf(is.String), + * is.OptionalOf(is.Boolean), + * ] as const); + * const a: unknown = [0, undefined, "a"]; + * if (isMyType(a)) { + * // a is narrowed to [number, string | undefined, boolean, number?, string?, boolean?] + * const _: [number, string | undefined, boolean, number?, string?, boolean?] = a; + * } + * ``` + * + * With `predElse`: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ParametersOf( + * [ + * is.Number, + * is.OptionalOf(is.String), + * is.OptionalOf(is.Boolean), + * ] as const, + * is.ArrayOf(is.Number), + * ); + * const a: unknown = [0, "a", true, 0, 1, 2]; + * if (isMyType(a)) { + * // a is narrowed to [number, string?, boolean?, ...number[]] + * const _: [number, string?, boolean?, ...number[]] = a; + * } + * ``` + * + * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array + * used as `predTup`. If a type error occurs, try adding `as const` as follows: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const predTup = [is.Number, is.String, is.OptionalOf(is.Boolean)] as const; + * const isMyType = is.ParametersOf(predTup); + * const a: unknown = [0, "a"]; + * if (isMyType(a)) { + * // a is narrowed to [number, string, boolean?] + * const _: [number, string, boolean?] = a; + * } + * ``` + */ +export function isParametersOf< + T extends readonly [...Predicate[]], +>( + predTup: T, +): Predicate> & WithMetadata; +export function isParametersOf< + T extends readonly [...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse: E, +): + & Predicate<[...ParametersOf, ...PredicateType]> + & WithMetadata; +export function isParametersOf< + T extends readonly [...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse?: E, +): + & Predicate | [...ParametersOf, ...PredicateType]> + & WithMetadata { + const requiresLength = 1 + predTup.findLastIndex((pred) => !isOptional(pred)); + if (!predElse) { + return setPredicateFactoryMetadata( + (x: unknown): x is ParametersOf => { + if ( + !isArray(x) || x.length < requiresLength || x.length > predTup.length + ) { + return false; + } + return predTup.every((pred, i) => pred(x[i])); + }, + { name: "isParametersOf", args: [predTup] }, + ); + } else { + return setPredicateFactoryMetadata( + (x: unknown): x is [...ParametersOf, ...PredicateType] => { + if (!isArray(x) || x.length < requiresLength) { + return false; + } + const head = x.slice(0, predTup.length); + const tail = x.slice(predTup.length); + return predTup.every((pred, i) => pred(head[i])) && predElse(tail); + }, + { name: "isParametersOf", args: [predTup, predElse] }, + ); + } +} + +type ParametersOf = T extends readonly [] ? [] + : T extends readonly [...infer P, infer R] + // Tuple of predicates + ? P extends Predicate[] + ? R extends Predicate & WithMetadata + // Last parameter is optional + ? [...ParametersOf

, PredicateType?] + // Last parameter is NOT optional + : [...ParametersOf

, PredicateType] + : never + // Array of predicates + : TupleOf; + +type IsParametersOfMetadata = { + name: "isParametersOf"; + args: [ + Parameters[0], + Parameters[1]?, + ]; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `Readonly>`. * @@ -1687,6 +1819,7 @@ export const is = { OneOf: isOneOf, Optional: isOptional, OptionalOf: isOptionalOf, + ParametersOf: isParametersOf, PartialOf: isPartialOf, PickOf: isPickOf, Primitive: isPrimitive, diff --git a/is_test.ts b/is_test.ts index 442e622..baf3e53 100644 --- a/is_test.ts +++ b/is_test.ts @@ -26,6 +26,7 @@ import { isOmitOf, isOneOf, isOptionalOf, + isParametersOf, isPartialOf, isPickOf, isPrimitive, @@ -633,6 +634,152 @@ Deno.test("isTupleOf", async (t) => { ); }); +Deno.test("isParametersOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isParametersOf([isNumber, isString, isOptionalOf(isBoolean)]).name, + ); + await assertSnapshot( + t, + isParametersOf([(_x): _x is string => false]).name, + ); + await assertSnapshot( + t, + isParametersOf([]).name, + ); + // Nested + await assertSnapshot( + t, + isParametersOf([ + isParametersOf([ + isParametersOf([isNumber, isString, isOptionalOf(isBoolean)]), + ]), + ]).name, + ); + }); + await t.step("returns proper type predicate", () => { + const predTup = [ + isOptionalOf(isNumber), + isString, + isOptionalOf(isString), + isOptionalOf(isBoolean), + ] as const; + const a: unknown = [0, "a"]; + if (isParametersOf(predTup)(a)) { + assertType< + Equal + >(true); + } + }); + await t.step("returns true on T tuple", () => { + const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; + assertEquals(isParametersOf(predTup)([0, "a", true]), true); + assertEquals(isParametersOf(predTup)([0, "a"]), true); + }); + await t.step("returns false on non T tuple", () => { + const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; + assertEquals(isParametersOf(predTup)([0, 1, 2]), false); + assertEquals(isParametersOf(predTup)([0, "a", true, 0]), false); + }); + await testWithExamples( + t, + isParametersOf([(_: unknown): _ is unknown => true]), + { + excludeExamples: ["array"], + }, + ); +}); + +Deno.test("isParametersOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isParametersOf([isNumber, isString, isOptionalOf(isBoolean)], isArray) + .name, + ); + await assertSnapshot( + t, + isParametersOf([(_x): _x is string => false], isArrayOf(isString)) + .name, + ); + // Empty + await assertSnapshot( + t, + isParametersOf([], isArrayOf(isString)).name, + ); + // Nested + await assertSnapshot( + t, + isParametersOf([ + isParametersOf( + [isParametersOf( + [isNumber, isString, isOptionalOf(isBoolean)], + isArray, + )], + isArray, + ), + ]).name, + ); + }); + await t.step("returns proper type predicate", () => { + const predTup = [ + isOptionalOf(isNumber), + isString, + isOptionalOf(isString), + isOptionalOf(isBoolean), + ] as const; + const predElse = isArrayOf(isNumber); + const a: unknown = [0, "a"]; + if (isParametersOf(predTup, predElse)(a)) { + assertType< + Equal< + typeof a, + [number | undefined, string, string?, boolean?, ...number[]] + > + >( + true, + ); + } + }); + await t.step("returns true on T tuple", () => { + const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; + const predElse = isArrayOf(isNumber); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), + true, + ); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), + true, + ); + assertEquals(isParametersOf(predTup, predElse)([0, "a"]), true); + }); + await t.step("returns false on non T tuple", () => { + const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; + const predElse = isArrayOf(isString); + assertEquals(isParametersOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); + assertEquals(isParametersOf(predTup, predElse)([0, "a", 0, 1, 2]), false); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), + false, + ); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), + false, + ); + assertEquals(isParametersOf(predTup, predElse)([0, "a", "b"]), false); + }); + const predElse = isArray; + await testWithExamples( + t, + isParametersOf([(_: unknown): _ is unknown => true], predElse), + { + excludeExamples: ["array"], + }, + ); +}); + Deno.test("isReadonlyTupleOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(