打字稿中的类型安全谓词功能



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

  1. 可以独立使用:array.filter(isNull)
  2. 可以在逻辑上组合:array.filter(and(not(isNull), not(isUndefined)))
  3. 使用type-guards,因此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);

但是,由于这里没有使用类型的指挥官,因此Typescript假定变量filtered具有与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;

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

但是,由于not(isNull)可能经常使用,所以我想将其提取到一个新的谓词功能中:

const isNotNull = not(isNull);

虽然这在运行时完美地工作,但不幸的是它没有编译(启用了严格模式的Tystecript 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方法typescright可以从数组中推断出I的类型,但是当将谓词提取到单独的函数中时,则此不再有效,并且Typescript将倒回基本对象类型{}破坏一切。

有办法解决此问题吗?说服打字稿粘在通用类型I而不是在定义isNotNull函数时将其解决到{}的一些技巧?还是这是打字稿的限制,目前无法完成?

刚刚在这里找到了我自己的两年历史的问题,并用最新的打字稿版本(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);

从上下文传递类型信息。此代码编译良好

// c: (string | number)[]
let c = [1, 2, 'b', 'a', null].filter(not<number | string | null, null>(isNull));