Skip to content

Commit 5b37cf4

Browse files
authored
Merge pull request #51 from lambdalisue/fix-oneof
🐛 Fix invalid type narrowed by `isOneOf` and add `as const` error for `isTupleOf`, etc.
2 parents 116aeb6 + 6d1dc94 commit 5b37cf4

File tree

2 files changed

+150
-134
lines changed

2 files changed

+150
-134
lines changed

is.ts

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export function isArrayOf<T>(
189189
* // type A = [string, number];
190190
* ```
191191
*/
192-
export type TupleOf<T extends readonly Predicate<unknown>[]> = {
192+
export type TupleOf<T> = {
193193
-readonly [P in keyof T]: T[P] extends Predicate<infer U> ? U : never;
194194
};
195195

@@ -204,37 +204,33 @@ export type TupleOf<T extends readonly Predicate<unknown>[]> = {
204204
* // type A = readonly [string, number];
205205
* ```
206206
*/
207-
export type ReadonlyTupleOf<T extends readonly Predicate<unknown>[]> = {
207+
export type ReadonlyTupleOf<T> = {
208208
[P in keyof T]: T[P] extends Predicate<infer U> ? U : never;
209209
};
210210

211211
/**
212-
* Return a type predicate function that returns `true` if the type of `x` is `TupleOf<T>`.
212+
* Return a type predicate function that returns `true` if the type of `x` is `TupleOf<T>` or `TupleOf<T, E>`.
213213
*
214214
* To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost.
215215
*
216216
* ```ts
217217
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
218218
*
219-
* const isMyType = is.TupleOf([is.Number, is.String, is.Boolean] as const);
219+
* const isMyType = is.TupleOf([is.Number, is.String, is.Boolean]);
220220
* const a: unknown = [0, "a", true];
221221
* if (isMyType(a)) {
222222
* // a is narrowed to [number, string, boolean]
223223
* const _: [number, string, boolean] = a;
224224
* }
225225
* ```
226226
*
227-
* Note that `predTup` must be `readonly` (`as const`) to infer the type of `a` correctly.
228-
* TypeScript won't argues if `predTup` is not `readonly` because of its design limitation.
229-
* https://github.com/microsoft/TypeScript/issues/34274#issuecomment-541691353
230-
*
231-
* It can also be used to check the type of the rest of the tuple like:
227+
* With `predElse`:
232228
*
233229
* ```ts
234230
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
235231
*
236232
* const isMyType = is.TupleOf(
237-
* [is.Number, is.String, is.Boolean] as const,
233+
* [is.Number, is.String, is.Boolean],
238234
* is.ArrayOf(is.Number),
239235
* );
240236
* const a: unknown = [0, "a", true, 0, 1, 2];
@@ -243,33 +239,44 @@ export type ReadonlyTupleOf<T extends readonly Predicate<unknown>[]> = {
243239
* const _: [number, string, boolean, ...number[]] = a;
244240
* }
245241
* ```
242+
*
243+
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
244+
* used as `predTup`. If a type error occurs, try adding `as const` as follows:
245+
*
246+
* ```ts
247+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
248+
*
249+
* const predTup = [is.Number, is.String, is.Boolean] as const;
250+
* const isMyType = is.TupleOf(predTup);
251+
* const a: unknown = [0, "a", true];
252+
* if (isMyType(a)) {
253+
* // a is narrowed to [number, string, boolean]
254+
* const _: [number, string, boolean] = a;
255+
* }
256+
* ```
246257
*/
247258
export function isTupleOf<
248-
T extends readonly Predicate<unknown>[],
249-
R extends TupleOf<T>,
259+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
250260
>(
251261
predTup: T,
252-
): Predicate<R>;
262+
): Predicate<TupleOf<T>>;
253263
export function isTupleOf<
254-
T extends readonly Predicate<unknown>[],
264+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
255265
E extends Predicate<unknown[]>,
256-
R extends [...TupleOf<T>, ...PredicateType<E>],
257266
>(
258267
predTup: T,
259268
predElse: E,
260-
): Predicate<R>;
269+
): Predicate<[...TupleOf<T>, ...PredicateType<E>]>;
261270
export function isTupleOf<
262-
T extends readonly Predicate<unknown>[],
271+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
263272
E extends Predicate<unknown[]>,
264-
R1 extends TupleOf<T>,
265-
R2 extends [...TupleOf<T>, ...PredicateType<E>],
266273
>(
267274
predTup: T,
268275
predElse?: E,
269-
): Predicate<R1 | R2> {
276+
): Predicate<TupleOf<T> | [...TupleOf<T>, ...PredicateType<E>]> {
270277
if (!predElse) {
271278
return Object.defineProperties(
272-
(x: unknown): x is R1 => {
279+
(x: unknown): x is TupleOf<T> => {
273280
if (!isArray(x) || x.length !== predTup.length) {
274281
return false;
275282
}
@@ -283,7 +290,7 @@ export function isTupleOf<
283290
);
284291
} else {
285292
return Object.defineProperties(
286-
(x: unknown): x is R2 => {
293+
(x: unknown): x is [...TupleOf<T>, ...PredicateType<E>] => {
287294
if (!isArray(x) || x.length < predTup.length) {
288295
return false;
289296
}
@@ -308,59 +315,69 @@ export function isTupleOf<
308315
* ```ts
309316
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
310317
*
311-
* const isMyType = is.ReadonlyTupleOf([is.Number, is.String, is.Boolean] as const);
318+
* const isMyType = is.ReadonlyTupleOf([is.Number, is.String, is.Boolean]);
312319
* const a: unknown = [0, "a", true];
313320
* if (isMyType(a)) {
314321
* // a is narrowed to readonly [number, string, boolean]
315322
* const _: readonly [number, string, boolean] = a;
316323
* }
317324
* ```
318325
*
319-
* Note that `predTup` must be `readonly` (`as const`) to infer the type of `a` correctly.
320-
* TypeScript won't argues if `predTup` is not `readonly` because of its design limitation.
321-
* https://github.com/microsoft/TypeScript/issues/34274#issuecomment-541691353
322-
*
323-
* It can also be used to check the type of the rest of the tuple like:
326+
* With `predElse`:
324327
*
325328
* ```ts
326329
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
327330
*
328331
* const isMyType = is.ReadonlyTupleOf(
329-
* [is.Number, is.String, is.Boolean] as const,
332+
* [is.Number, is.String, is.Boolean],
330333
* is.ArrayOf(is.Number),
331334
* );
332335
* const a: unknown = [0, "a", true, 0, 1, 2];
333336
* if (isMyType(a)) {
334337
* // a is narrowed to readonly [number, string, boolean, ...number[]]
335338
* const _: readonly [number, string, boolean, ...number[]] = a;
336339
* }
340+
* ```
341+
*
342+
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
343+
* used as `predTup`. If a type error occurs, try adding `as const` as follows:
344+
*
345+
* ```ts
346+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
347+
*
348+
* const predTup = [is.Number, is.String, is.Boolean] as const;
349+
* const isMyType = is.ReadonlyTupleOf(predTup);
350+
* const a: unknown = [0, "a", true];
351+
* if (isMyType(a)) {
352+
* // a is narrowed to readonly [number, string, boolean]
353+
* const _: readonly [number, string, boolean] = a;
354+
* }
355+
* ```
337356
*/
338357
export function isReadonlyTupleOf<
339-
T extends readonly Predicate<unknown>[],
340-
R extends ReadonlyTupleOf<T>,
358+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
341359
>(
342360
predTup: T,
343-
): Predicate<R>;
361+
): Predicate<ReadonlyTupleOf<T>>;
344362
export function isReadonlyTupleOf<
345-
T extends readonly Predicate<unknown>[],
363+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
346364
E extends Predicate<unknown[]>,
347-
R extends readonly [...ReadonlyTupleOf<T>, ...PredicateType<E>],
348365
>(
349366
predTup: T,
350367
predElse: E,
351-
): Predicate<R>;
368+
): Predicate<readonly [...ReadonlyTupleOf<T>, ...PredicateType<E>]>;
352369
export function isReadonlyTupleOf<
353-
T extends readonly Predicate<unknown>[],
370+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
354371
E extends Predicate<unknown[]>,
355-
R1 extends ReadonlyTupleOf<T>,
356-
R2 extends readonly [...ReadonlyTupleOf<T>, ...PredicateType<E>],
357372
>(
358373
predTup: T,
359374
predElse?: E,
360-
): Predicate<R1 | R2> {
375+
): Predicate<
376+
ReadonlyTupleOf<T> | readonly [...ReadonlyTupleOf<T>, ...PredicateType<E>]
377+
> {
361378
if (!predElse) {
362379
return Object.defineProperties(
363-
isTupleOf(predTup) as Predicate<R1>,
380+
isTupleOf(predTup) as Predicate<ReadonlyTupleOf<T>>,
364381
{
365382
name: {
366383
get: () => `isReadonlyTupleOf(${inspect(predTup)})`,
@@ -369,7 +386,9 @@ export function isReadonlyTupleOf<
369386
);
370387
} else {
371388
return Object.defineProperties(
372-
isTupleOf(predTup, predElse) as unknown as Predicate<R2>,
389+
isTupleOf(predTup, predElse) as unknown as Predicate<
390+
readonly [...ReadonlyTupleOf<T>, ...PredicateType<E>]
391+
>,
373392
{
374393
name: {
375394
get: () =>
@@ -449,9 +468,13 @@ export function isUniformTupleOf<T, N extends number>(
449468
n: N,
450469
pred: Predicate<T> = isAny,
451470
): Predicate<UniformTupleOf<T, N>> {
452-
const predInner = isTupleOf(Array(n).fill(pred));
453471
return Object.defineProperties(
454-
(x: unknown): x is UniformTupleOf<T, N> => predInner(x),
472+
(x: unknown): x is UniformTupleOf<T, N> => {
473+
if (!isArray(x) || x.length !== n) {
474+
return false;
475+
}
476+
return x.every((v) => pred(v));
477+
},
455478
{
456479
name: {
457480
get: () => `isUniformTupleOf(${n}, ${inspect(pred)})`,
@@ -935,7 +958,9 @@ export function isLiteralOneOf<T extends readonly Primitive[]>(
935958
);
936959
}
937960

938-
export type OneOf<T> = T extends Predicate<infer U>[] ? U : never;
961+
export type OneOf<T> = T extends readonly [Predicate<infer U>, ...infer R]
962+
? U | OneOf<R>
963+
: never;
939964

940965
/**
941966
* Return a type predicate function that returns `true` if the type of `x` is `OneOf<T>`.
@@ -952,8 +977,25 @@ export type OneOf<T> = T extends Predicate<infer U>[] ? U : never;
952977
* const _: number | string | boolean = a;
953978
* }
954979
* ```
980+
*
981+
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
982+
* used as `preds`. If a type error occurs, try adding `as const` as follows:
983+
*
984+
* ```ts
985+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
986+
*
987+
* const preds = [is.Number, is.String, is.Boolean] as const;
988+
* const isMyType = is.OneOf(preds);
989+
* const a: unknown = 0;
990+
* if (isMyType(a)) {
991+
* // a is narrowed to number | string | boolean
992+
* const _: number | string | boolean = a;
993+
* }
994+
* ```
955995
*/
956-
export function isOneOf<T extends readonly Predicate<unknown>[]>(
996+
export function isOneOf<
997+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
998+
>(
957999
preds: T,
9581000
): Predicate<OneOf<T>> {
9591001
return Object.defineProperties(
@@ -986,12 +1028,32 @@ export type AllOf<T> = UnionToIntersection<OneOf<T>>;
9861028
* ]);
9871029
* const a: unknown = { a: 0, b: "a" };
9881030
* if (isMyType(a)) {
989-
* // a is narrowed to { a: number; b: string }
990-
* const _: { a: number; b: string } = a;
1031+
* // a is narrowed to { a: number } & { b: string }
1032+
* const _: { a: number } & { b: string } = a;
1033+
* }
1034+
* ```
1035+
*
1036+
* Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array
1037+
* used as `preds`. If a type error occurs, try adding `as const` as follows:
1038+
*
1039+
* ```ts
1040+
* import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts";
1041+
*
1042+
* const preds = [
1043+
* is.ObjectOf({ a: is.Number }),
1044+
* is.ObjectOf({ b: is.String }),
1045+
* ] as const
1046+
* const isMyType = is.AllOf(preds);
1047+
* const a: unknown = { a: 0, b: "a" };
1048+
* if (isMyType(a)) {
1049+
* // a is narrowed to { a: number } & { b: string }
1050+
* const _: { a: number } & { b: string } = a;
9911051
* }
9921052
* ```
9931053
*/
994-
export function isAllOf<T extends readonly Predicate<unknown>[]>(
1054+
export function isAllOf<
1055+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
1056+
>(
9951057
preds: T,
9961058
): Predicate<AllOf<T>> {
9971059
return Object.defineProperties(

0 commit comments

Comments
 (0)