Skip to content

Commit 9c00592

Browse files
committed
👍 Add isParametersOf
1 parent ec8f83e commit 9c00592

File tree

3 files changed

+319
-0
lines changed

3 files changed

+319
-0
lines changed

__snapshots__/is_test.ts.snap

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,54 @@ snapshot[`isTupleOf<T, E> > returns properly named function 3`] = `
7070
])"
7171
`;
7272
73+
snapshot[`isParametersOf<T> > returns properly named function 1`] = `
74+
"isParametersOf([
75+
isNumber,
76+
isString,
77+
isOptionalOf(isBoolean)
78+
])"
79+
`;
80+
81+
snapshot[`isParametersOf<T> > returns properly named function 2`] = `"isParametersOf([(anonymous)])"`;
82+
83+
snapshot[`isParametersOf<T> > returns properly named function 3`] = `"isParametersOf([])"`;
84+
85+
snapshot[`isParametersOf<T> > returns properly named function 4`] = `
86+
"isParametersOf([
87+
isParametersOf([
88+
isParametersOf([
89+
isNumber,
90+
isString,
91+
isOptionalOf(isBoolean)
92+
])
93+
])
94+
])"
95+
`;
96+
97+
snapshot[`isParametersOf<T, E> > returns properly named function 1`] = `
98+
"isParametersOf([
99+
isNumber,
100+
isString,
101+
isOptionalOf(isBoolean)
102+
], isArray)"
103+
`;
104+
105+
snapshot[`isParametersOf<T, E> > returns properly named function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`;
106+
107+
snapshot[`isParametersOf<T, E> > returns properly named function 3`] = `"isParametersOf([], isArrayOf(isString))"`;
108+
109+
snapshot[`isParametersOf<T, E> > returns properly named function 4`] = `
110+
"isParametersOf([
111+
isParametersOf([
112+
isParametersOf([
113+
isNumber,
114+
isString,
115+
isOptionalOf(isBoolean)
116+
], isArray)
117+
], isArray)
118+
])"
119+
`;
120+
73121
snapshot[`isReadonlyTupleOf<T> > returns properly named function 1`] = `
74122
"isReadonlyOf(isTupleOf([
75123
isNumber,

is.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,129 @@ type IsTupleOfMetadata = {
751751
args: [Parameters<typeof isTupleOf>[0], Parameters<typeof isTupleOf>[1]?];
752752
};
753753

754+
/**
755+
* Return a type predicate function that returns `true` if the type of `x` is `ParametersOf<T>` or `ParametersOf<T, E>`.
756+
*
757+
* This is similar to `TupleOf<T>` or `TupleOf<T, E>`, except that the `OptionalOf<P>` at the end of the tuple becomes optional.
758+
*
759+
* To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost.
760+
*
761+
* ```ts
762+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
763+
*
764+
* const isMyType = is.ParametersOf(
765+
* [is.Number, is.String, is.OptionalOf(is.Boolean)] as const,
766+
* );
767+
* const a: unknown = [0, "a"];
768+
* if (isMyType(a)) {
769+
* // a is narrowed to [number, string, boolean?]
770+
* const _: [number, string, boolean?] = a;
771+
* }
772+
* ```
773+
*
774+
* With `predElse`:
775+
*
776+
* ```ts
777+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
778+
*
779+
* const isMyType = is.ParametersOf(
780+
* [is.Number, is.OptionalOf(is.String), is.OptionalOf(is.Boolean)] as const,
781+
* is.ArrayOf(is.Number),
782+
* );
783+
* const a: unknown = [0, "a", true, 0, 1, 2];
784+
* if (isMyType(a)) {
785+
* // a is narrowed to [number, string?, boolean?, ...number[]]
786+
* const _: [number, string?, boolean?, ...number[]] = a;
787+
* }
788+
* ```
789+
*
790+
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
791+
* used as `predTup`. If a type error occurs, try adding `as const` as follows:
792+
*
793+
* ```ts
794+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
795+
*
796+
* const predTup = [is.Number, is.String, is.OptionalOf(is.Boolean)] as const;
797+
* const isMyType = is.ParametersOf(predTup);
798+
* const a: unknown = [0, "a"];
799+
* if (isMyType(a)) {
800+
* // a is narrowed to [number, string, boolean?]
801+
* const _: [number, string, boolean?] = a;
802+
* }
803+
* ```
804+
*/
805+
export function isParametersOf<
806+
T extends readonly [...Predicate<unknown>[]],
807+
>(
808+
predTup: T,
809+
): Predicate<ParametersOf<T>> & WithMetadata<IsParametersOfMetadata>;
810+
export function isParametersOf<
811+
T extends readonly [...Predicate<unknown>[]],
812+
E extends Predicate<unknown[]>,
813+
>(
814+
predTup: T,
815+
predElse: E,
816+
):
817+
& Predicate<[...ParametersOf<T>, ...PredicateType<E>]>
818+
& WithMetadata<IsParametersOfMetadata>;
819+
export function isParametersOf<
820+
T extends readonly [...Predicate<unknown>[]],
821+
E extends Predicate<unknown[]>,
822+
>(
823+
predTup: T,
824+
predElse?: E,
825+
):
826+
& Predicate<ParametersOf<T> | [...ParametersOf<T>, ...PredicateType<E>]>
827+
& WithMetadata<IsParametersOfMetadata> {
828+
const requiresLength = 1 + predTup.findLastIndex((pred) => !isOptional(pred));
829+
if (!predElse) {
830+
return setPredicateFactoryMetadata(
831+
(x: unknown): x is ParametersOf<T> => {
832+
if (
833+
!isArray(x) || x.length < requiresLength || x.length > predTup.length
834+
) {
835+
return false;
836+
}
837+
return predTup.every((pred, i) => pred(x[i]));
838+
},
839+
{ name: "isParametersOf", args: [predTup] },
840+
);
841+
} else {
842+
return setPredicateFactoryMetadata(
843+
(x: unknown): x is [...ParametersOf<T>, ...PredicateType<E>] => {
844+
if (!isArray(x) || x.length < requiresLength) {
845+
return false;
846+
}
847+
const head = x.slice(0, predTup.length);
848+
const tail = x.slice(predTup.length);
849+
return predTup.every((pred, i) => pred(head[i])) && predElse(tail);
850+
},
851+
{ name: "isParametersOf", args: [predTup, predElse] },
852+
);
853+
}
854+
}
855+
856+
type ParametersOf<T> = T extends readonly [] ? []
857+
: T extends readonly [...infer P, infer R]
858+
// Tuple of predicates
859+
? P extends Predicate<unknown>[]
860+
? R extends Predicate<unknown> & WithMetadata<IsOptionalOfMetadata>
861+
// Last parameter is optional
862+
? [...ParametersOf<P>, PredicateType<R>?]
863+
// Last parameter is NOT optional
864+
: [...ParametersOf<P>, PredicateType<R>]
865+
: never
866+
// Array of predicates
867+
: TupleOf<T>;
868+
869+
type IsParametersOfMetadata = {
870+
name: "isParametersOf";
871+
args: [
872+
Parameters<typeof isParametersOf>[0],
873+
Parameters<typeof isParametersOf>[1]?,
874+
];
875+
};
876+
754877
/**
755878
* Return a type predicate function that returns `true` if the type of `x` is `Readonly<TupleOf<T>>`.
756879
*
@@ -1637,6 +1760,7 @@ export const is = {
16371760
OneOf: isOneOf,
16381761
Optional: isOptional,
16391762
OptionalOf: isOptionalOf,
1763+
ParametersOf: isParametersOf,
16401764
PartialOf: isPartialOf,
16411765
PickOf: isPickOf,
16421766
Primitive: isPrimitive,

is_test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
isOmitOf,
2727
isOneOf,
2828
isOptionalOf,
29+
isParametersOf,
2930
isPartialOf,
3031
isPickOf,
3132
isPrimitive,
@@ -633,6 +634,152 @@ Deno.test("isTupleOf<T, E>", async (t) => {
633634
);
634635
});
635636

637+
Deno.test("isParametersOf<T>", async (t) => {
638+
await t.step("returns properly named function", async (t) => {
639+
await assertSnapshot(
640+
t,
641+
isParametersOf([isNumber, isString, isOptionalOf(isBoolean)]).name,
642+
);
643+
await assertSnapshot(
644+
t,
645+
isParametersOf([(_x): _x is string => false]).name,
646+
);
647+
await assertSnapshot(
648+
t,
649+
isParametersOf([]).name,
650+
);
651+
// Nested
652+
await assertSnapshot(
653+
t,
654+
isParametersOf([
655+
isParametersOf([
656+
isParametersOf([isNumber, isString, isOptionalOf(isBoolean)]),
657+
]),
658+
]).name,
659+
);
660+
});
661+
await t.step("returns proper type predicate", () => {
662+
const predTup = [
663+
isOptionalOf(isNumber),
664+
isString,
665+
isOptionalOf(isString),
666+
isOptionalOf(isBoolean),
667+
] as const;
668+
const a: unknown = [0, "a"];
669+
if (isParametersOf(predTup)(a)) {
670+
assertType<
671+
Equal<typeof a, [number | undefined, string, string?, boolean?]>
672+
>(true);
673+
}
674+
});
675+
await t.step("returns true on T tuple", () => {
676+
const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const;
677+
assertEquals(isParametersOf(predTup)([0, "a", true]), true);
678+
assertEquals(isParametersOf(predTup)([0, "a"]), true);
679+
});
680+
await t.step("returns false on non T tuple", () => {
681+
const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const;
682+
assertEquals(isParametersOf(predTup)([0, 1, 2]), false);
683+
assertEquals(isParametersOf(predTup)([0, "a", true, 0]), false);
684+
});
685+
await testWithExamples(
686+
t,
687+
isParametersOf([(_: unknown): _ is unknown => true]),
688+
{
689+
excludeExamples: ["array"],
690+
},
691+
);
692+
});
693+
694+
Deno.test("isParametersOf<T, E>", async (t) => {
695+
await t.step("returns properly named function", async (t) => {
696+
await assertSnapshot(
697+
t,
698+
isParametersOf([isNumber, isString, isOptionalOf(isBoolean)], isArray)
699+
.name,
700+
);
701+
await assertSnapshot(
702+
t,
703+
isParametersOf([(_x): _x is string => false], isArrayOf(isString))
704+
.name,
705+
);
706+
// Empty
707+
await assertSnapshot(
708+
t,
709+
isParametersOf([], isArrayOf(isString)).name,
710+
);
711+
// Nested
712+
await assertSnapshot(
713+
t,
714+
isParametersOf([
715+
isParametersOf(
716+
[isParametersOf(
717+
[isNumber, isString, isOptionalOf(isBoolean)],
718+
isArray,
719+
)],
720+
isArray,
721+
),
722+
]).name,
723+
);
724+
});
725+
await t.step("returns proper type predicate", () => {
726+
const predTup = [
727+
isOptionalOf(isNumber),
728+
isString,
729+
isOptionalOf(isString),
730+
isOptionalOf(isBoolean),
731+
] as const;
732+
const predElse = isArrayOf(isNumber);
733+
const a: unknown = [0, "a"];
734+
if (isParametersOf(predTup, predElse)(a)) {
735+
assertType<
736+
Equal<
737+
typeof a,
738+
[number | undefined, string, string?, boolean?, ...number[]]
739+
>
740+
>(
741+
true,
742+
);
743+
}
744+
});
745+
await t.step("returns true on T tuple", () => {
746+
const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const;
747+
const predElse = isArrayOf(isNumber);
748+
assertEquals(
749+
isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]),
750+
true,
751+
);
752+
assertEquals(
753+
isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]),
754+
true,
755+
);
756+
assertEquals(isParametersOf(predTup, predElse)([0, "a"]), true);
757+
});
758+
await t.step("returns false on non T tuple", () => {
759+
const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const;
760+
const predElse = isArrayOf(isString);
761+
assertEquals(isParametersOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false);
762+
assertEquals(isParametersOf(predTup, predElse)([0, "a", 0, 1, 2]), false);
763+
assertEquals(
764+
isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]),
765+
false,
766+
);
767+
assertEquals(
768+
isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]),
769+
false,
770+
);
771+
assertEquals(isParametersOf(predTup, predElse)([0, "a", "b"]), false);
772+
});
773+
const predElse = isArray;
774+
await testWithExamples(
775+
t,
776+
isParametersOf([(_: unknown): _ is unknown => true], predElse),
777+
{
778+
excludeExamples: ["array"],
779+
},
780+
);
781+
});
782+
636783
Deno.test("isReadonlyTupleOf<T>", async (t) => {
637784
await t.step("returns properly named function", async (t) => {
638785
await assertSnapshot(

0 commit comments

Comments
 (0)