Моя цель - написать функции предикатов (например, isNull
и isUndefined
) в TypeScript, которые удовлетворяют следующим условиям:
- Может использоваться автономно:
array.filter(isNull)
- Может быть логически объединено:
array.filter(and(not(isNull), not(isUndefined)))
- Использует Type-Guard, поэтому TypeScript знает, например, что тип возвращаемого значения
array.filter(isNull)
будет 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)[]
.
Поэтому я добавил магию машинописи:
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 и не может быть сделано в настоящее время?