TypeScript 中的类型安全谓词函数
Type-safe predicate functions in TypeScript
我的目标是在满足以下条件的 TypeScript 中编写谓词函数(例如 isNull
和 isUndefined
):
- 可独立使用:
array.filter(isNull)
- 可以逻辑组合:
array.filter(and(not(isNull), not(isUndefined)))
- 使用类型保护,因此 TypeScript 知道
array.filter(isNull)
的 return 类型将是 null[]
- 可以在不破坏类型推断的情况下将组合谓词提取到新的谓词函数中:
const isNotNull = not(isNull)
前两个条件很容易满足:
type Predicate = (i: any) => boolean;
const and = (p1: Predicate, p2: Predicate) =>
(i: any) => p1(i) && p2(i);
const or = (p1: Predicate, p2: Predicate) =>
(i: any) => p1(i) || p2(i);
const not = (p: Predicate) =>
(i: any) => !p(i);
const isNull = (i: any) =>
i === null;
const isUndefined = (i: any) =>
i === undefined;
const items = [ "foo", null, 123, undefined, true ];
const filtered = items.filter(and(not(isNull), not(isUndefined)));
console.log(filtered);
但是因为这里没有使用类型保护,TypeScript 假定变量 filtered
与 items
具有相同的类型,即 (string,number,boolean,null,undefined)[]
而实际上现在应该是 (string,number,boolean)[]
.
所以我添加了一些 typescript 魔法:
type Diff<T, U> = T extends U ? never : T;
type Predicate<I, O extends I> = (i: I) => i is O;
const and = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 & O2) => p1(i) && p2(i);
const or = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 | O2) => p1(i) || p2(i);
const not = <I, O extends I>(p: Predicate<I, O>) =>
(i: I): i is (Diff<I, O>) => !p(i);
const isNull = <I>(i: I | null): i is null =>
i === null;
const isUndefined = <I>(i: I | undefined): i is undefined =>
i === undefined;
现在它似乎可以工作了,filtered
被正确地缩减为类型 (string,number,boolean)[]
。
但是因为 not(isNull)
可能会经常使用,所以我想将其提取到一个新的谓词函数中:
const isNotNull = not(isNull);
虽然这在运行时完美运行,但不幸的是它无法编译(启用严格模式的 TypeScript 3.3.3):
Argument of type '<I>(i: I | null) => i is null' is not assignable to parameter of type 'Predicate<{}, {}>'.
Type predicate 'i is null' is not assignable to 'i is {}'.
Type 'null' is not assignable to type '{}'.ts(2345)
所以我猜想在使用谓词作为数组 filter
方法的参数时,TypeScript 可以从数组中推断出 I
的类型,但是当将谓词提取到一个单独的函数中时,这不再有效并且 TypeScript 回退到基础对象类型 {}
这打破了一切。
有办法解决这个问题吗?在定义 isNotNull
函数时,有什么技巧可以说服 TypeScript 坚持泛型类型 I
而不是将其解析为 {}
吗?或者这是 TypeScript 的限制,目前无法完成?
从上下文传递类型信息。这段代码编译的不错
// c: (string | number)[]
let c = [1, 2, 'b', 'a', null].filter(not<number | string | null, null>(isNull));
刚刚在这里发现了我自己两年前的问题,并用最新的 TypeScript 版本 (4.3.5) 再次尝试,问题不再存在。以下代码编译良好并且类型被正确推断:
type Diff<T, U> = T extends U ? never : T;
type Predicate<I, O extends I> = (i: I) => i is O;
const and = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 & O2) => p1(i) && p2(i);
const or = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 | O2) => p1(i) || p2(i);
const not = <I, O extends I>(p: Predicate<I, O>) =>
(i: I): i is (Diff<I, O>) => !p(i);
const isNull = <I>(i: I | null): i is null =>
i === null;
const isUndefined = <I>(i: I | undefined): i is undefined =>
i === undefined;
const isNotNull = not(isNull);
const isNotUndefined = not(isUndefined);
const items = [ "foo", null, 123, undefined, true ];
const filtered = items.filter(and(isNotNull, isNotUndefined));
console.log(filtered);
我的目标是在满足以下条件的 TypeScript 中编写谓词函数(例如 isNull
和 isUndefined
):
- 可独立使用:
array.filter(isNull)
- 可以逻辑组合:
array.filter(and(not(isNull), not(isUndefined)))
- 使用类型保护,因此 TypeScript 知道
array.filter(isNull)
的 return 类型将是null[]
- 可以在不破坏类型推断的情况下将组合谓词提取到新的谓词函数中:
const isNotNull = not(isNull)
前两个条件很容易满足:
type Predicate = (i: any) => boolean;
const and = (p1: Predicate, p2: Predicate) =>
(i: any) => p1(i) && p2(i);
const or = (p1: Predicate, p2: Predicate) =>
(i: any) => p1(i) || p2(i);
const not = (p: Predicate) =>
(i: any) => !p(i);
const isNull = (i: any) =>
i === null;
const isUndefined = (i: any) =>
i === undefined;
const items = [ "foo", null, 123, undefined, true ];
const filtered = items.filter(and(not(isNull), not(isUndefined)));
console.log(filtered);
但是因为这里没有使用类型保护,TypeScript 假定变量 filtered
与 items
具有相同的类型,即 (string,number,boolean,null,undefined)[]
而实际上现在应该是 (string,number,boolean)[]
.
所以我添加了一些 typescript 魔法:
type Diff<T, U> = T extends U ? never : T;
type Predicate<I, O extends I> = (i: I) => i is O;
const and = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 & O2) => p1(i) && p2(i);
const or = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 | O2) => p1(i) || p2(i);
const not = <I, O extends I>(p: Predicate<I, O>) =>
(i: I): i is (Diff<I, O>) => !p(i);
const isNull = <I>(i: I | null): i is null =>
i === null;
const isUndefined = <I>(i: I | undefined): i is undefined =>
i === undefined;
现在它似乎可以工作了,filtered
被正确地缩减为类型 (string,number,boolean)[]
。
但是因为 not(isNull)
可能会经常使用,所以我想将其提取到一个新的谓词函数中:
const isNotNull = not(isNull);
虽然这在运行时完美运行,但不幸的是它无法编译(启用严格模式的 TypeScript 3.3.3):
Argument of type '<I>(i: I | null) => i is null' is not assignable to parameter of type 'Predicate<{}, {}>'.
Type predicate 'i is null' is not assignable to 'i is {}'.
Type 'null' is not assignable to type '{}'.ts(2345)
所以我猜想在使用谓词作为数组 filter
方法的参数时,TypeScript 可以从数组中推断出 I
的类型,但是当将谓词提取到一个单独的函数中时,这不再有效并且 TypeScript 回退到基础对象类型 {}
这打破了一切。
有办法解决这个问题吗?在定义 isNotNull
函数时,有什么技巧可以说服 TypeScript 坚持泛型类型 I
而不是将其解析为 {}
吗?或者这是 TypeScript 的限制,目前无法完成?
从上下文传递类型信息。这段代码编译的不错
// c: (string | number)[]
let c = [1, 2, 'b', 'a', null].filter(not<number | string | null, null>(isNull));
刚刚在这里发现了我自己两年前的问题,并用最新的 TypeScript 版本 (4.3.5) 再次尝试,问题不再存在。以下代码编译良好并且类型被正确推断:
type Diff<T, U> = T extends U ? never : T;
type Predicate<I, O extends I> = (i: I) => i is O;
const and = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 & O2) => p1(i) && p2(i);
const or = <I, O1 extends I, O2 extends I>(p1: Predicate<I, O1>, p2: Predicate<I, O2>) =>
(i: I): i is (O1 | O2) => p1(i) || p2(i);
const not = <I, O extends I>(p: Predicate<I, O>) =>
(i: I): i is (Diff<I, O>) => !p(i);
const isNull = <I>(i: I | null): i is null =>
i === null;
const isUndefined = <I>(i: I | undefined): i is undefined =>
i === undefined;
const isNotNull = not(isNull);
const isNotUndefined = not(isUndefined);
const items = [ "foo", null, 123, undefined, true ];
const filtered = items.filter(and(isNotNull, isNotUndefined));
console.log(filtered);