Skip to content

Commit d94d0c2

Browse files
authored
Merge pull request #74 from Milly/parametersof
👍 Add `isParametersOf`
2 parents d4f1d1d + e46fff3 commit d94d0c2

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,138 @@ 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>`, but if `is.OptionalOf()` is specified at the trailing, the trailing elements becomes optional and makes variable-length tuple.
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,
766+
* is.OptionalOf(is.String),
767+
* is.Boolean,
768+
* is.OptionalOf(is.Number),
769+
* is.OptionalOf(is.String),
770+
* is.OptionalOf(is.Boolean),
771+
* ] as const);
772+
* const a: unknown = [0, undefined, "a"];
773+
* if (isMyType(a)) {
774+
* // a is narrowed to [number, string | undefined, boolean, number?, string?, boolean?]
775+
* const _: [number, string | undefined, boolean, number?, string?, boolean?] = a;
776+
* }
777+
* ```
778+
*
779+
* With `predElse`:
780+
*
781+
* ```ts
782+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
783+
*
784+
* const isMyType = is.ParametersOf(
785+
* [
786+
* is.Number,
787+
* is.OptionalOf(is.String),
788+
* is.OptionalOf(is.Boolean),
789+
* ] as const,
790+
* is.ArrayOf(is.Number),
791+
* );
792+
* const a: unknown = [0, "a", true, 0, 1, 2];
793+
* if (isMyType(a)) {
794+
* // a is narrowed to [number, string?, boolean?, ...number[]]
795+
* const _: [number, string?, boolean?, ...number[]] = a;
796+
* }
797+
* ```
798+
*
799+
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
800+
* used as `predTup`. If a type error occurs, try adding `as const` as follows:
801+
*
802+
* ```ts
803+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
804+
*
805+
* const predTup = [is.Number, is.String, is.OptionalOf(is.Boolean)] as const;
806+
* const isMyType = is.ParametersOf(predTup);
807+
* const a: unknown = [0, "a"];
808+
* if (isMyType(a)) {
809+
* // a is narrowed to [number, string, boolean?]
810+
* const _: [number, string, boolean?] = a;
811+
* }
812+
* ```
813+
*/
814+
export function isParametersOf<
815+
T extends readonly [...Predicate<unknown>[]],
816+
>(
817+
predTup: T,
818+
): Predicate<ParametersOf<T>> & 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>, ...PredicateType<E>]>
827+
& WithMetadata<IsParametersOfMetadata>;
828+
export function isParametersOf<
829+
T extends readonly [...Predicate<unknown>[]],
830+
E extends Predicate<unknown[]>,
831+
>(
832+
predTup: T,
833+
predElse?: E,
834+
):
835+
& Predicate<ParametersOf<T> | [...ParametersOf<T>, ...PredicateType<E>]>
836+
& WithMetadata<IsParametersOfMetadata> {
837+
const requiresLength = 1 + predTup.findLastIndex((pred) => !isOptional(pred));
838+
if (!predElse) {
839+
return setPredicateFactoryMetadata(
840+
(x: unknown): x is ParametersOf<T> => {
841+
if (
842+
!isArray(x) || x.length < requiresLength || x.length > predTup.length
843+
) {
844+
return false;
845+
}
846+
return predTup.every((pred, i) => pred(x[i]));
847+
},
848+
{ name: "isParametersOf", args: [predTup] },
849+
);
850+
} else {
851+
return setPredicateFactoryMetadata(
852+
(x: unknown): x is [...ParametersOf<T>, ...PredicateType<E>] => {
853+
if (!isArray(x) || x.length < requiresLength) {
854+
return false;
855+
}
856+
const head = x.slice(0, predTup.length);
857+
const tail = x.slice(predTup.length);
858+
return predTup.every((pred, i) => pred(head[i])) && predElse(tail);
859+
},
860+
{ name: "isParametersOf", args: [predTup, predElse] },
861+
);
862+
}
863+
}
864+
865+
type ParametersOf<T> = T extends readonly [] ? []
866+
: T extends readonly [...infer P, infer R]
867+
// Tuple of predicates
868+
? P extends Predicate<unknown>[]
869+
? R extends Predicate<unknown> & WithMetadata<IsOptionalOfMetadata>
870+
// Last parameter is optional
871+
? [...ParametersOf<P>, PredicateType<R>?]
872+
// Last parameter is NOT optional
873+
: [...ParametersOf<P>, PredicateType<R>]
874+
: never
875+
// Array of predicates
876+
: TupleOf<T>;
877+
878+
type IsParametersOfMetadata = {
879+
name: "isParametersOf";
880+
args: [
881+
Parameters<typeof isParametersOf>[0],
882+
Parameters<typeof isParametersOf>[1]?,
883+
];
884+
};
885+
754886
/**
755887
* Return a type predicate function that returns `true` if the type of `x` is `Readonly<TupleOf<T>>`.
756888
*
@@ -1687,6 +1819,7 @@ export const is = {
16871819
OneOf: isOneOf,
16881820
Optional: isOptional,
16891821
OptionalOf: isOptionalOf,
1822+
ParametersOf: isParametersOf,
16901823
PartialOf: isPartialOf,
16911824
PickOf: isPickOf,
16921825
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)