Skip to content

Commit

Permalink
Merge pull request #74 from Milly/parametersof
Browse files Browse the repository at this point in the history
👍 Add `isParametersOf`
  • Loading branch information
lambdalisue authored Apr 14, 2024
2 parents d4f1d1d + e46fff3 commit d94d0c2
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 0 deletions.
48 changes: 48 additions & 0 deletions __snapshots__/is_test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,54 @@ snapshot[`isTupleOf<T, E> > returns properly named function 3`] = `
])"
`;
snapshot[`isParametersOf<T> > returns properly named function 1`] = `
"isParametersOf([
isNumber,
isString,
isOptionalOf(isBoolean)
])"
`;
snapshot[`isParametersOf<T> > returns properly named function 2`] = `"isParametersOf([(anonymous)])"`;
snapshot[`isParametersOf<T> > returns properly named function 3`] = `"isParametersOf([])"`;
snapshot[`isParametersOf<T> > returns properly named function 4`] = `
"isParametersOf([
isParametersOf([
isParametersOf([
isNumber,
isString,
isOptionalOf(isBoolean)
])
])
])"
`;
snapshot[`isParametersOf<T, E> > returns properly named function 1`] = `
"isParametersOf([
isNumber,
isString,
isOptionalOf(isBoolean)
], isArray)"
`;
snapshot[`isParametersOf<T, E> > returns properly named function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`;
snapshot[`isParametersOf<T, E> > returns properly named function 3`] = `"isParametersOf([], isArrayOf(isString))"`;
snapshot[`isParametersOf<T, E> > returns properly named function 4`] = `
"isParametersOf([
isParametersOf([
isParametersOf([
isNumber,
isString,
isOptionalOf(isBoolean)
], isArray)
], isArray)
])"
`;
snapshot[`isReadonlyTupleOf<T> > returns properly named function 1`] = `
"isReadonlyOf(isTupleOf([
isNumber,
Expand Down
133 changes: 133 additions & 0 deletions is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,138 @@ type IsTupleOfMetadata = {
args: [Parameters<typeof isTupleOf>[0], Parameters<typeof isTupleOf>[1]?];
};

/**
* Return a type predicate function that returns `true` if the type of `x` is `ParametersOf<T>` or `ParametersOf<T, E>`.
*
* This is similar to `TupleOf<T>` or `TupleOf<T, E>`, 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<unknown>[]],
>(
predTup: T,
): Predicate<ParametersOf<T>> & WithMetadata<IsParametersOfMetadata>;
export function isParametersOf<
T extends readonly [...Predicate<unknown>[]],
E extends Predicate<unknown[]>,
>(
predTup: T,
predElse: E,
):
& Predicate<[...ParametersOf<T>, ...PredicateType<E>]>
& WithMetadata<IsParametersOfMetadata>;
export function isParametersOf<
T extends readonly [...Predicate<unknown>[]],
E extends Predicate<unknown[]>,
>(
predTup: T,
predElse?: E,
):
& Predicate<ParametersOf<T> | [...ParametersOf<T>, ...PredicateType<E>]>
& WithMetadata<IsParametersOfMetadata> {
const requiresLength = 1 + predTup.findLastIndex((pred) => !isOptional(pred));
if (!predElse) {
return setPredicateFactoryMetadata(
(x: unknown): x is ParametersOf<T> => {
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<T>, ...PredicateType<E>] => {
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> = T extends readonly [] ? []
: T extends readonly [...infer P, infer R]
// Tuple of predicates
? P extends Predicate<unknown>[]
? R extends Predicate<unknown> & WithMetadata<IsOptionalOfMetadata>
// Last parameter is optional
? [...ParametersOf<P>, PredicateType<R>?]
// Last parameter is NOT optional
: [...ParametersOf<P>, PredicateType<R>]
: never
// Array of predicates
: TupleOf<T>;

type IsParametersOfMetadata = {
name: "isParametersOf";
args: [
Parameters<typeof isParametersOf>[0],
Parameters<typeof isParametersOf>[1]?,
];
};

/**
* Return a type predicate function that returns `true` if the type of `x` is `Readonly<TupleOf<T>>`.
*
Expand Down Expand Up @@ -1687,6 +1819,7 @@ export const is = {
OneOf: isOneOf,
Optional: isOptional,
OptionalOf: isOptionalOf,
ParametersOf: isParametersOf,
PartialOf: isPartialOf,
PickOf: isPickOf,
Primitive: isPrimitive,
Expand Down
147 changes: 147 additions & 0 deletions is_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
isOmitOf,
isOneOf,
isOptionalOf,
isParametersOf,
isPartialOf,
isPickOf,
isPrimitive,
Expand Down Expand Up @@ -633,6 +634,152 @@ Deno.test("isTupleOf<T, E>", async (t) => {
);
});

Deno.test("isParametersOf<T>", 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<typeof a, [number | undefined, string, string?, boolean?]>
>(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<T, E>", 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<T>", async (t) => {
await t.step("returns properly named function", async (t) => {
await assertSnapshot(
Expand Down

0 comments on commit d94d0c2

Please sign in to comment.