Безопасные по типу функции предикатов в TypeScript - PullRequest
0 голосов
/ 21 февраля 2019

Моя цель - написать функции предикатов (например, isNull и isUndefined) в TypeScript, которые удовлетворяют следующим условиям:

  1. Может использоваться автономно: array.filter(isNull)
  2. Может быть логически объединено: array.filter(and(not(isNull), not(isUndefined)))
  3. Использует Type-Guard, поэтому 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);

Пока это отлично работает при запускевремя, к сожалению, не компилируется (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 возвращается к базовому типу объекта {}, который нарушает все.

Есть ли способ исправить это?Какой-нибудь трюк, чтобы убедить TypeScript придерживаться универсального типа I вместо разрешения его в {} при определении функции isNotNull?Или это ограничение TypeScript и не может быть сделано в настоящее время?

Ответы [ 2 ]

0 голосов
/ 22 февраля 2019

Передача информации о типе из контекста.Этот код хорошо скомпилирован

// c: (string | number)[]
let c = [1, 2, 'b', 'a', null].filter(not<number | string | null, null>(isNull)); 
0 голосов
/ 21 февраля 2019

Это то, что вы ищете?

const isNotNull = not(<Predicate<any, null>>isNull);

...