diff --git a/is/__snapshots__/tuple_of_test.ts.snap b/is/__snapshots__/tuple_of_test.ts.snap index ff7bd39..6d1b65e 100644 --- a/is/__snapshots__/tuple_of_test.ts.snap +++ b/is/__snapshots__/tuple_of_test.ts.snap @@ -39,7 +39,69 @@ snapshot[`isTupleOf > returns properly named predicate function 3`] = ` isNumber, isString, isBoolean - ], isArray) + ], isArrayOf(isString)) ], isArray) ])" `; + +snapshot[`isTupleOf > returns properly named predicate function 1`] = ` +"isTupleOf(isArray, [ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isTupleOf > returns properly named predicate function 2`] = `"isTupleOf(isArrayOf(isString), [(anonymous)])"`; + +snapshot[`isTupleOf > returns properly named predicate function 3`] = ` +"isTupleOf([ + isTupleOf(isArray, [ + isTupleOf(isArrayOf(isString), [ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + +snapshot[`isTupleOf > returns properly named predicate function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +], isArray, [ + isString, + isBoolean, + isNumber +])" +`; + +snapshot[`isTupleOf > returns properly named predicate function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString), [(anonymous)])"`; + +snapshot[`isTupleOf > returns properly named predicate function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ], isArrayOf(isString), [ + isString, + isBoolean, + isNumber + ]) + ], isArray, [ + isTupleOf([ + isNumber, + isString, + isBoolean + ], isArrayOf(isNumber), [ + isNumber, + isBoolean, + isString + ]) + ]) +])" +`; diff --git a/is/mod.ts b/is/mod.ts index 8952e39..3d1b896 100644 --- a/is/mod.ts +++ b/is/mod.ts @@ -823,7 +823,7 @@ export const is: { */ SyncFunction: typeof isSyncFunction; /** - * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. + * Return a type predicate function that returns `true` if the type of `x` is `TupleOf`. * * Use {@linkcode isUniformTupleOf} to check if the type of `x` is a tuple of uniform types. * @@ -839,7 +839,7 @@ export const is: { * } * ``` * - * With `predRest` to represent rest elements: + * With `predRest` to represent rest elements or leading rest elements: * * ```ts * import { is } from "@core/unknownutil"; @@ -852,6 +852,30 @@ export const is: { * if (isMyType(a)) { * const _: [number, string, boolean, ...number[]] = a; * } + * + * const isMyTypeLeadingRest = is.TupleOf( + * is.ArrayOf(is.Number), + * [is.Number, is.String, is.Boolean], + * ); + * if (isMyTypeLeadingRest(a)) { + * const _: [...number[], number, string, boolean] = a; + * } + * ``` + * + * With `predRest` and `predTrail` to represent middle rest elements: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.TupleOf( + * [is.Number, is.String, is.Boolean], + * is.ArrayOf(is.Number), + * [is.Number, is.String, is.Boolean], + * ); + * const a: unknown = [0, "a", true, 0, 1, 2, 0, "a", true]; + * if (isMyType(a)) { + * const _: [number, string, boolean, ...number[], number, string, boolean] = a; + * } * ``` * * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array diff --git a/is/tuple_of.ts b/is/tuple_of.ts index bed8c17..1748484 100644 --- a/is/tuple_of.ts +++ b/is/tuple_of.ts @@ -3,7 +3,7 @@ import type { Predicate, PredicateType } from "../type.ts"; import { isArray } from "./array.ts"; /** - * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. + * Return a type predicate function that returns `true` if the type of `x` is `TupleOf`. * * Use {@linkcode isUniformTupleOf} to check if the type of `x` is a tuple of uniform types. * @@ -19,7 +19,7 @@ import { isArray } from "./array.ts"; * } * ``` * - * With `predRest` to represent rest elements: + * With `predRest` to represent rest elements or leading rest elements: * * ```ts * import { is } from "@core/unknownutil"; @@ -32,6 +32,30 @@ import { isArray } from "./array.ts"; * if (isMyType(a)) { * const _: [number, string, boolean, ...number[]] = a; * } + * + * const isMyTypeLeadingRest = is.TupleOf( + * is.ArrayOf(is.Number), + * [is.Number, is.String, is.Boolean], + * ); + * if (isMyTypeLeadingRest(a)) { + * const _: [...number[], number, string, boolean] = a; + * } + * ``` + * + * With `predRest` and `predTrail` to represent middle rest elements: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.TupleOf( + * [is.Number, is.String, is.Boolean], + * is.ArrayOf(is.Number), + * [is.Number, is.String, is.Boolean], + * ); + * const a: unknown = [0, "a", true, 0, 1, 2, 0, "a", true]; + * if (isMyType(a)) { + * const _: [number, string, boolean, ...number[], number, string, boolean] = a; + * } * ``` * * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array @@ -62,13 +86,57 @@ export function isTupleOf< predRest: R, ): Predicate<[...TupleOf, ...PredicateType]>; +export function isTupleOf< + R extends Predicate, + T extends readonly [Predicate, ...Predicate[]], +>( + predRest: R, + predTup: T, +): Predicate<[...PredicateType, ...TupleOf]>; + export function isTupleOf< T extends readonly [Predicate, ...Predicate[]], R extends Predicate, + L extends readonly [Predicate, ...Predicate[]], >( predTup: T, - predRest?: R, -): Predicate | [...TupleOf, ...PredicateType]> { + predRest: R, + predTrail: L, +): Predicate<[...TupleOf, ...PredicateType, ...TupleOf]>; + +export function isTupleOf< + T extends readonly [Predicate, ...Predicate[]], + R extends Predicate, + L extends readonly [Predicate, ...Predicate[]], +>( + predTupOrRest: T | R, + predRestOrTup?: R | T, + predTrail?: L, +): Predicate< + | TupleOf + | [...TupleOf, ...PredicateType] + | [...PredicateType, ...TupleOf] + | [...TupleOf, ...PredicateType, ...TupleOf] +> { + if (typeof predTupOrRest === "function") { + const predRest = predTupOrRest as R; + const predTup = predRestOrTup as T; + return rewriteName( + (x: unknown): x is [...PredicateType, ...TupleOf] => { + if (!isArray(x) || x.length < predTup.length) { + return false; + } + const offset = x.length - predTup.length; + return predTup.every((pred, i) => pred(x[offset + i])) && + predRest(x.slice(0, offset)); + }, + "isTupleOf", + predRest, + predTup, + ); + } + const predTup = predTupOrRest as T; + const predRest = predRestOrTup as R; if (!predRest) { return rewriteName( (x: unknown): x is TupleOf => { @@ -80,19 +148,36 @@ export function isTupleOf< "isTupleOf", predTup, ); - } else { + } else if (!predTrail) { return rewriteName( (x: unknown): x is [...TupleOf, ...PredicateType] => { if (!isArray(x) || x.length < predTup.length) { return false; } - const head = x.slice(0, predTup.length); - const tail = x.slice(predTup.length); - return predTup.every((pred, i) => pred(head[i])) && predRest(tail); + return predTup.every((pred, i) => pred(x[i])) && + predRest(x.slice(predTup.length)); + }, + "isTupleOf", + predTup, + predRest, + ); + } else { + return rewriteName( + ( + x: unknown, + ): x is [...TupleOf, ...PredicateType, ...TupleOf] => { + if (!isArray(x) || x.length < (predTup.length + predTrail.length)) { + return false; + } + const offset = x.length - predTrail.length; + return predTup.every((pred, i) => pred(x[i])) && + predTrail.every((pred, i) => pred(x[offset + i])) && + predRest(x.slice(predTup.length, offset)); }, "isTupleOf", predTup, predRest, + predTrail, ); } } diff --git a/is/tuple_of_test.ts b/is/tuple_of_test.ts index e273efc..b6bc7f5 100644 --- a/is/tuple_of_test.ts +++ b/is/tuple_of_test.ts @@ -17,8 +17,11 @@ Deno.test("isTupleOf", async (t) => { ); await assertSnapshot( t, - isTupleOf([isTupleOf([isTupleOf([is.Number, is.String, is.Boolean])])]) - .name, + isTupleOf([ + isTupleOf([ + isTupleOf([is.Number, is.String, is.Boolean]), + ]), + ]).name, ); }); @@ -51,14 +54,21 @@ Deno.test("isTupleOf", async (t) => { ); await assertSnapshot( t, - isTupleOf([(_x): _x is string => false], is.ArrayOf(is.String)) - .name, + isTupleOf( + [(_x): _x is string => false], + is.ArrayOf(is.String), + ).name, ); await assertSnapshot( t, isTupleOf([ isTupleOf( - [isTupleOf([is.Number, is.String, is.Boolean], is.Array)], + [ + isTupleOf( + [is.Number, is.String, is.Boolean], + is.ArrayOf(is.String), + ), + ], is.Array, ), ]).name, @@ -68,19 +78,39 @@ Deno.test("isTupleOf", async (t) => { await t.step("returns true on T tuple", () => { const predTup = [is.Number, is.String, is.Boolean] as const; const predRest = is.ArrayOf(is.Number); + assertEquals(isTupleOf(predTup, predRest)([0, "a", true]), true); assertEquals(isTupleOf(predTup, predRest)([0, "a", true, 0, 1, 2]), true); }); await t.step("returns false on non T tuple", () => { const predTup = [is.Number, is.String, is.Boolean] as const; const predRest = is.ArrayOf(is.String); - assertEquals(isTupleOf(predTup, predRest)([0, 1, 2, 0, 1, 2]), false); - assertEquals(isTupleOf(predTup, predRest)([0, "a", 0, 1, 2]), false); + assertEquals(isTupleOf(predTup, predRest)("a"), false, "Not an array"); + assertEquals( + isTupleOf(predTup, predRest)([0, "a"]), + false, + "Less than `predTup.length`", + ); assertEquals( - isTupleOf(predTup, predRest)([0, "a", true, 0, 0, 1, 2]), + isTupleOf(predTup, predRest)([0, 1, 2]), false, + "Not match `predTup` and no rest elements", + ); + assertEquals( + isTupleOf(predTup, predRest)([0, 1, 2, 0, 1, 2]), + false, + "Not match `predTup` and `predRest`", + ); + assertEquals( + isTupleOf(predTup, predRest)([0, "a", true, 0, 1, 2]), + false, + "Match `predTup` but not match `predRest`", + ); + assertEquals( + isTupleOf(predTup, predRest)([0, "a", "b", "a", "b", "c"]), + false, + "Match `predRest` but not match `predTup`", ); - assertEquals(isTupleOf(predTup, predRest)([0, "a", true, 0, 1, 2]), false); }); await t.step("predicated type is correct", () => { @@ -94,3 +124,210 @@ Deno.test("isTupleOf", async (t) => { } }); }); + +Deno.test("isTupleOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot( + t, + isTupleOf(is.Array, [is.Number, is.String, is.Boolean]).name, + ); + await assertSnapshot( + t, + isTupleOf( + is.ArrayOf(is.String), + [(_x): _x is string => false], + ).name, + ); + await assertSnapshot( + t, + isTupleOf([ + isTupleOf( + is.Array, + [ + isTupleOf( + is.ArrayOf(is.String), + [is.Number, is.String, is.Boolean], + ), + ], + ), + ]).name, + ); + }); + + await t.step("returns true on T tuple", () => { + const predRest = is.ArrayOf(is.Number); + const predTup = [is.Number, is.String, is.Boolean] as const; + assertEquals(isTupleOf(predRest, predTup)([0, "a", true]), true); + assertEquals(isTupleOf(predRest, predTup)([0, 1, 2, 0, "a", true]), true); + }); + + await t.step("returns false on non T tuple", () => { + const predRest = is.ArrayOf(is.String); + const predTup = [is.Number, is.String, is.Boolean] as const; + assertEquals(isTupleOf(predRest, predTup)("a"), false, "Not an array"); + assertEquals( + isTupleOf(predRest, predTup)([0, "a"]), + false, + "Less than `predTup.length`", + ); + assertEquals( + isTupleOf(predRest, predTup)([0, 1, 2]), + false, + "Not match `predTup` and no rest elements", + ); + assertEquals( + isTupleOf(predRest, predTup)([0, 1, 2, 0, 1, 2]), + false, + "Not match `predTup` and `predRest`", + ); + assertEquals( + isTupleOf(predRest, predTup)([0, 1, 2, 0, "a", true]), + false, + "Match `predTup` but not match `predRest`", + ); + assertEquals( + isTupleOf(predRest, predTup)(["a", "b", "c", 0, "a", "b"]), + false, + "Match `predRest` but not match `predTup`", + ); + }); + + await t.step("predicated type is correct", () => { + const predRest = is.ArrayOf(is.Number); + const predTup = [is.Number, is.String, is.Boolean] as const; + const a: unknown = [0, 1, 2, 0, "a", true]; + if (isTupleOf(predRest, predTup)(a)) { + assertType>( + true, + ); + } + }); +}); + +Deno.test("isTupleOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot( + t, + isTupleOf( + [is.Number, is.String, is.Boolean], + is.Array, + [is.String, is.Boolean, is.Number], + ).name, + ); + await assertSnapshot( + t, + isTupleOf( + [(_x): _x is string => false], + is.ArrayOf(is.String), + [(_x): _x is string => false], + ).name, + ); + await assertSnapshot( + t, + isTupleOf([ + isTupleOf( + [ + isTupleOf( + [is.Number, is.String, is.Boolean], + is.ArrayOf(is.String), + [is.String, is.Boolean, is.Number], + ), + ], + is.Array, + [ + isTupleOf( + [is.Number, is.String, is.Boolean], + is.ArrayOf(is.Number), + [is.Number, is.Boolean, is.String], + ), + ], + ), + ]).name, + ); + }); + + await t.step("returns true on T tuple", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const predRest = is.ArrayOf(is.Number); + const predTrail = [is.Number, is.String, is.Boolean] as const; + assertEquals( + isTupleOf(predTup, predRest, predTrail)([0, "a", true, 0, "a", true]), + true, + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)( + [0, "a", true, 0, 1, 2, 0, "a", true], + ), + true, + ); + }); + + await t.step("returns false on non T tuple", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const predRest = is.ArrayOf(is.String); + const predTrail = [is.Number, is.String, is.Boolean] as const; + assertEquals( + isTupleOf(predTup, predRest, predTrail)("a"), + false, + "Not an array", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)([0, "a", true, 0, "a"]), + false, + "Less than `predTup.length + predTrail.length`", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)([0, 1, 2, 0, 1, 2, 0, 1, 2]), + false, + "Not match `predTup`, `predRest` and `predTrail`", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)([0, "a", true, 0, "a", "b"]), + false, + "Match `predTup` but not match `predTrail` and no rest elements", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)([0, "a", "b", 0, "a", true]), + false, + "Match `predTrail` but not match `predTup` and no rest elements", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)( + [0, "a", true, 0, 1, 2, 0, "a", true], + ), + false, + "Match `predTup` and `predTrail` but not match `predRest`", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)( + [0, "a", true, "a", "b", "c", 0, "a", "b"], + ), + false, + "Match `predTup` and `predRest` but not match `predTrail`", + ); + assertEquals( + isTupleOf(predTup, predRest, predTrail)( + [0, "a", "b", "a", "b", "c", 0, "a", true], + ), + false, + "Match `predRest` and `predTrail` but not match `predTup`", + ); + }); + + await t.step("predicated type is correct", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const predRest = is.ArrayOf(is.Number); + const predTrail = [is.Number, is.Boolean] as const; + const a: unknown = [0, "a", true, 0, 1, 2, 0, true]; + if (isTupleOf(predTup, predRest, predTrail)(a)) { + assertType< + Equal< + typeof a, + [number, string, boolean, ...number[], number, boolean] + > + >( + true, + ); + } + }); +});