TypeScript 中的类型安全谓词函数

kay*_*ahr 10 predicate typescript

我的目标是在 TypeScript 中编写满足以下条件的谓词函数(例如)isNullisUndefined

  1. 可以独立使用:array.filter(isNull)
  2. 可以逻辑组合:array.filter(and(not(isNull), not(isUndefined)))
  3. 使用类型保护,因此 TypeScript 知道 的返回类型array.filter(isNull)将是null[]
  4. 组合谓词可以提取到新的谓词函数中,而不会破坏类型推断: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);
Run Code Online (Sandbox Code Playgroud)

但因为这里没有使用类型保护,TypeScript 假设该变量具有与isfiltered相同的类型,而实际上它现在应该是。items(string,number,boolean,null,undefined)[](string,number,boolean)[]

所以我添加了一些打字稿魔法:

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;
Run Code Online (Sandbox Code Playgroud)

现在它似乎可以工作,filtered被正确地简化为 type (string,number,boolean)[]

但因为not(isNull)可能经常使用,我想将其提取到一个新的谓词函数中:

const isNotNull = not(isNull);
Run Code Online (Sandbox Code Playgroud)

虽然这在运行时完美工作,但不幸的是它无法编译(启用严格模式的 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)
Run Code Online (Sandbox Code Playgroud)

所以我猜想,当使用谓词作为数组filter方法的参数时,TypeScript 可以从数组中推断出类型,I但是当将谓词提取到单独的函数中时,这不再有效,TypeScript 会回退到基本对象类型,{}这会破坏一切。

有没有办法来解决这个问题?在定义函数时,有什么技巧可以说服 TypeScript 坚持使用泛型类型I而不是解析它吗?或者这是 TypeScript 的限制,目前无法完成?{}isNotNull

kay*_*ahr 15

刚刚在这里找到了我自己两年前的问题,并使用最新的 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);
Run Code Online (Sandbox Code Playgroud)