Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1637,6 +1769,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