My goal is to write predicate functions (isNull
and isUndefined
for example) in TypeScript which fulfill the following conditions:
- Can be used standalone:
array.filter(isNull)
- Can be logically combined:
array.filter(and(not(isNull), not(isUndefined)))
- Uses Type-guards so TypeScript knows for example that the return type of
array.filter(isNull)
will benull[]
- Combined predicates can be extracted into new predicate functions without breaking type inference:
const isNotNull = not(isNull)
The first two conditions are easy to fulfill:
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);
But because no type-guards are used here TypeScript assumes that the variable filtered
has the same type as items
which is (string,number,boolean,null,undefined)[]
while it actually now should be (string,number,boolean)[]
.
So I added some typescript magic:
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;
Now it seems to work, filtered
is correctly reduced to type (string,number,boolean)[]
.
But because not(isNull)
might be used quite often I want to extract this into a new predicate function:
const isNotNull = not(isNull);
While this perfectly works at runtime unfortunately it doesn't compile (TypeScript 3.3.3 with strict mode enabled):
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)
So I guess while using the predicates as argument for the arrays filter
method TypeScript can infer the type of I
from the array but when extracting the predicate into a separate function then this no longer works and TypeScript falls back to the base object type {}
which breaks everything.
Is there a way to fix this? Some trick to convince TypeScript to stick to the generic type I
instead of resolving it to {}
when defining the isNotNull
function? Or is this a limitation of TypeScript and can't be done currently?