Skip to content

Commit 6d1dc94

Browse files
committed
🐛 Fix invalid type narrowed by isOneOf
Close #49 Because of this bug fix, `isOneOf` and `isAllOf` now requires a readonly array of predicates. Thus users may face some type errors when updating this module. In that case, just add `as const` to the array of predicates.
1 parent e198e43 commit 6d1dc94

File tree

2 files changed

+51
-12
lines changed

2 files changed

+51
-12
lines changed

is.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,9 @@ export function isLiteralOneOf<T extends readonly Primitive[]>(
958958
);
959959
}
960960

961-
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;
962964

963965
/**
964966
* Return a type predicate function that returns `true` if the type of `x` is `OneOf<T>`.
@@ -975,8 +977,25 @@ export type OneOf<T> = T extends Predicate<infer U>[] ? U : never;
975977
* const _: number | string | boolean = a;
976978
* }
977979
* ```
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+
* ```
978995
*/
979-
export function isOneOf<T extends readonly Predicate<unknown>[]>(
996+
export function isOneOf<
997+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
998+
>(
980999
preds: T,
9811000
): Predicate<OneOf<T>> {
9821001
return Object.defineProperties(
@@ -1009,12 +1028,32 @@ export type AllOf<T> = UnionToIntersection<OneOf<T>>;
10091028
* ]);
10101029
* const a: unknown = { a: 0, b: "a" };
10111030
* if (isMyType(a)) {
1012-
* // a is narrowed to { a: number; b: string }
1013-
* 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;
10141051
* }
10151052
* ```
10161053
*/
1017-
export function isAllOf<T extends readonly Predicate<unknown>[]>(
1054+
export function isAllOf<
1055+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
1056+
>(
10181057
preds: T,
10191058
): Predicate<AllOf<T>> {
10201059
return Object.defineProperties(

is_test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,7 @@ Deno.test("isOneOf<T>", async (t) => {
934934
await assertSnapshot(t, isOneOf([isNumber, isString, isBoolean]).name);
935935
});
936936
await t.step("returns proper type predicate", () => {
937-
const preds = [isNumber, isString, isBoolean];
937+
const preds = [isNumber, isString, isBoolean] as const;
938938
const a: unknown = [0, "a", true];
939939
if (isOneOf(preds)(a)) {
940940
assertType<Equal<typeof a, number | string | boolean>>(true);
@@ -945,20 +945,20 @@ Deno.test("isOneOf<T>", async (t) => {
945945
const isBar = isObjectOf({ foo: isString, bar: isNumber });
946946
type Foo = PredicateType<typeof isFoo>;
947947
type Bar = PredicateType<typeof isBar>;
948-
const preds = [isFoo, isBar];
948+
const preds = [isFoo, isBar] as const;
949949
const a: unknown = [0, "a", true];
950950
if (isOneOf(preds)(a)) {
951951
assertType<Equal<typeof a, Foo | Bar>>(true);
952952
}
953953
});
954954
await t.step("returns true on one of T", () => {
955-
const preds = [isNumber, isString, isBoolean];
955+
const preds = [isNumber, isString, isBoolean] as const;
956956
assertEquals(isOneOf(preds)(0), true);
957957
assertEquals(isOneOf(preds)("a"), true);
958958
assertEquals(isOneOf(preds)(true), true);
959959
});
960960
await t.step("returns false on non of T", async (t) => {
961-
const preds = [isNumber, isString, isBoolean];
961+
const preds = [isNumber, isString, isBoolean] as const;
962962
await testWithExamples(t, isOneOf(preds), {
963963
excludeExamples: ["number", "string", "boolean"],
964964
});
@@ -979,7 +979,7 @@ Deno.test("isAllOf<T>", async (t) => {
979979
const preds = [
980980
is.ObjectOf({ a: is.Number }),
981981
is.ObjectOf({ b: is.String }),
982-
];
982+
] as const;
983983
const a: unknown = { a: 0, b: "a" };
984984
if (isAllOf(preds)(a)) {
985985
assertType<Equal<typeof a, { a: number } & { b: string }>>(true);
@@ -989,14 +989,14 @@ Deno.test("isAllOf<T>", async (t) => {
989989
const preds = [
990990
is.ObjectOf({ a: is.Number }),
991991
is.ObjectOf({ b: is.String }),
992-
];
992+
] as const;
993993
assertEquals(isAllOf(preds)({ a: 0, b: "a" }), true);
994994
});
995995
await t.step("returns false on non of T", async (t) => {
996996
const preds = [
997997
is.ObjectOf({ a: is.Number }),
998998
is.ObjectOf({ b: is.String }),
999-
];
999+
] as const;
10001000
assertEquals(
10011001
isAllOf(preds)({ a: 0, b: 0 }),
10021002
false,

0 commit comments

Comments
 (0)