Skip to content

Commit bd69b53

Browse files
committed
feat[isTupleOf]: add overload to middle rest elements
1 parent 774bc21 commit bd69b53

File tree

3 files changed

+198
-2
lines changed

3 files changed

+198
-2
lines changed

is/__snapshots__/tuple_of_test.ts.snap

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,35 @@ snapshot[`isTupleOf<T, R> > returns properly named predicate function 3`] = `
4343
], isArray)
4444
])"
4545
`;
46+
47+
snapshot[`isTupleOf<T, R, L> > returns properly named predicate function 1`] = `
48+
"isTupleOf([
49+
isNumber,
50+
isString,
51+
isBoolean
52+
], isArray, [
53+
isNumber,
54+
isString,
55+
isBoolean
56+
])"
57+
`;
58+
59+
snapshot[`isTupleOf<T, R, L> > returns properly named predicate function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString), [(anonymous)])"`;
60+
61+
snapshot[`isTupleOf<T, R, L> > returns properly named predicate function 3`] = `
62+
"isTupleOf([
63+
isTupleOf([
64+
isTupleOf([
65+
isNumber,
66+
isString,
67+
isBoolean
68+
], isArray)
69+
], isArray, [
70+
isTupleOf([
71+
isNumber,
72+
isString,
73+
isBoolean
74+
], isArray)
75+
])
76+
])"
77+
`;

is/tuple_of.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ import { isArray } from "./array.ts";
3434
* }
3535
* ```
3636
*
37+
* With `predRest` and `predTrail` to represent middle rest elements:
38+
*
39+
* ```ts
40+
* import { is } from "@core/unknownutil";
41+
*
42+
* const isMyType = is.TupleOf(
43+
* [is.Number, is.String, is.Boolean],
44+
* is.ArrayOf(is.Number),
45+
* [is.Number, is.String, is.Boolean],
46+
* );
47+
* const a: unknown = [0, "a", true, 0, 1, 2, 0, "a", true];
48+
* if (isMyType(a)) {
49+
* const _: [number, string, boolean, ...number[], number, string, boolean] = a;
50+
* }
51+
* ```
52+
*
3753
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
3854
* used as `predTup`. If a type error occurs, try adding `as const` as follows:
3955
*
@@ -65,10 +81,26 @@ export function isTupleOf<
6581
export function isTupleOf<
6682
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
6783
R extends Predicate<unknown[]>,
84+
L extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
85+
>(
86+
predTup: T,
87+
predRest: R,
88+
predTrail: L,
89+
): Predicate<[...TupleOf<T>, ...PredicateType<R>, ...TupleOf<T>]>;
90+
91+
export function isTupleOf<
92+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
93+
R extends Predicate<unknown[]>,
94+
L extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
6895
>(
6996
predTup: T,
7097
predRest?: R,
71-
): Predicate<TupleOf<T> | [...TupleOf<T>, ...PredicateType<R>]> {
98+
predTrail?: L,
99+
): Predicate<
100+
| TupleOf<T>
101+
| [...TupleOf<T>, ...PredicateType<R>]
102+
| [...TupleOf<T>, ...PredicateType<R>, ...TupleOf<T>]
103+
> {
72104
if (!predRest) {
73105
return rewriteName(
74106
(x: unknown): x is TupleOf<T> => {
@@ -80,7 +112,7 @@ export function isTupleOf<
80112
"isTupleOf",
81113
predTup,
82114
);
83-
} else {
115+
} else if (!predTrail) {
84116
return rewriteName(
85117
(x: unknown): x is [...TupleOf<T>, ...PredicateType<R>] => {
86118
if (!isArray(x) || x.length < predTup.length) {
@@ -93,6 +125,23 @@ export function isTupleOf<
93125
predTup,
94126
predRest,
95127
);
128+
} else {
129+
return rewriteName(
130+
(x: unknown): x is [...TupleOf<T>, ...PredicateType<R>] => {
131+
if (!isArray(x) || x.length < (predTup.length + predTrail.length)) {
132+
return false;
133+
}
134+
const rest = x.slice(predTup.length, -predTrail.length);
135+
const trail = x.slice(-predTrail.length);
136+
return predTup.every((pred, i) => pred(x[i])) &&
137+
predTrail.every((pred, i) => pred(trail[i])) &&
138+
predRest(rest);
139+
},
140+
"isTupleOf",
141+
predTup,
142+
predRest,
143+
predTrail,
144+
);
96145
}
97146
}
98147

is/tuple_of_test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,118 @@ Deno.test("isTupleOf<T, R>", async (t) => {
114114
}
115115
});
116116
});
117+
118+
Deno.test("isTupleOf<T, R, L>", async (t) => {
119+
await t.step("returns properly named predicate function", async (t) => {
120+
await assertSnapshot(
121+
t,
122+
isTupleOf([is.Number, is.String, is.Boolean], is.Array, [
123+
is.Number,
124+
is.String,
125+
is.Boolean,
126+
]).name,
127+
);
128+
await assertSnapshot(
129+
t,
130+
isTupleOf([(_x): _x is string => false], is.ArrayOf(is.String), [
131+
(_x): _x is string => false,
132+
])
133+
.name,
134+
);
135+
await assertSnapshot(
136+
t,
137+
isTupleOf([
138+
isTupleOf(
139+
[isTupleOf([is.Number, is.String, is.Boolean], is.Array)],
140+
is.Array,
141+
[isTupleOf([is.Number, is.String, is.Boolean], is.Array)],
142+
),
143+
]).name,
144+
);
145+
});
146+
147+
await t.step("returns true on T tuple", () => {
148+
const predTup = [is.Number, is.String, is.Boolean] as const;
149+
const predRest = is.ArrayOf(is.Number);
150+
const predTrail = [is.Number, is.String, is.Boolean] as const;
151+
assertEquals(
152+
isTupleOf(predTup, predRest, predTrail)([0, "a", true, 0, "a", true]),
153+
true,
154+
);
155+
assertEquals(
156+
isTupleOf(predTup, predRest, predTrail)(
157+
[0, "a", true, 0, 1, 2, 0, "a", true],
158+
),
159+
true,
160+
);
161+
});
162+
163+
await t.step("returns false on non T tuple", () => {
164+
const predTup = [is.Number, is.String, is.Boolean] as const;
165+
const predRest = is.ArrayOf(is.String);
166+
const predTrail = [is.Number, is.String, is.Boolean] as const;
167+
assertEquals(
168+
isTupleOf(predTup, predRest, predTrail)("a"),
169+
false,
170+
"Not an array",
171+
);
172+
assertEquals(
173+
isTupleOf(predTup, predRest, predTrail)([0, "a", true, 0, "a"]),
174+
false,
175+
"Less than `predTup.length + predTrail.length`",
176+
);
177+
assertEquals(
178+
isTupleOf(predTup, predRest, predTrail)([0, 1, 2, 0, 1, 2, 0, 1, 2]),
179+
false,
180+
"Not match `predTup`, `predRest` and `predTrail`",
181+
);
182+
assertEquals(
183+
isTupleOf(predTup, predRest, predTrail)([0, "a", true, 0, "a", "b"]),
184+
false,
185+
"Match `predTup` but not match `predTrail` and no rest elements",
186+
);
187+
assertEquals(
188+
isTupleOf(predTup, predRest, predTrail)([0, "a", "b", 0, "a", true]),
189+
false,
190+
"Match `predTrail` but not match `predTup` and no rest elements",
191+
);
192+
assertEquals(
193+
isTupleOf(predTup, predRest, predTrail)(
194+
[0, "a", true, 0, 1, 2, 0, "a", true],
195+
),
196+
false,
197+
"Match `predTup` and `predTrail` but not match `predRest`",
198+
);
199+
assertEquals(
200+
isTupleOf(predTup, predRest, predTrail)(
201+
[0, "a", true, "a", "b", "c", 0, "a", "b"],
202+
),
203+
false,
204+
"Match `predTup` and `predRest` but not match `predTrail`",
205+
);
206+
assertEquals(
207+
isTupleOf(predTup, predRest, predTrail)(
208+
[0, "a", "b", "a", "b", "c", 0, "a", true],
209+
),
210+
false,
211+
"Match `predRest` and `predTrail` but not match `predTup`",
212+
);
213+
});
214+
215+
await t.step("predicated type is correct", () => {
216+
const predTup = [is.Number, is.String, is.Boolean] as const;
217+
const predRest = is.ArrayOf(is.Number);
218+
const predTrail = [is.Number, is.String, is.Boolean] as const;
219+
const a: unknown = [0, "a", true, 0, 1, 2, 0, "a", true];
220+
if (isTupleOf(predTup, predRest, predTrail)(a)) {
221+
assertType<
222+
Equal<
223+
typeof a,
224+
[number, string, boolean, ...number[], number, string, boolean]
225+
>
226+
>(
227+
true,
228+
);
229+
}
230+
});
231+
});

0 commit comments

Comments
 (0)