My goal is to write predicate functions (isNull
and isUndefined
for example) in TypeScript which fulfill the following conditions:
array.filter(isNull)
array.filter(and(not(isNull), not(isUndefined)))
array.filter(isNull)
will be null[]
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?
Just found my own two year old question here and tried it again with latest TypeScript version (4.3.5) and the problem does no longer exist. The following code compiles fine and the types are correctly inferred:
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With