Typescript gives developers confidence in their code changes. The types help developers understand code better, refactor code easier etc. However, one of the pain points of typescript is the is
keyword.
is
?is
is called a type guard
const isPen = (x: any): x is Pen => {
return x instanceof Pen
}
const p: Pen | undefined | null = getPen()
if (isPen(p)) {
// typescript knows that `p` is `Pen`
} else {
// typescript knows that `p` is `undefined | null`
}
You can see this in typescript playground
Using this with filter
we can filter a list of Pen | undefined | null
to a list of Pen
.
const ps: (Pen | undefined | null)[] = getPens()
const pens = ps.filter(isPen) // typescript knows that `pens` is `Pen[]`
However, the problem with this pattern is that Typescript does not care if the implementation of a type guard is correct.
const isPen = (x: any): x is Pen => true
In this case, isPen
is obviously wrong, but typescript will still assume that x
will be Pen
if isPen
returns true. This does not give developers good confidence about the types.
This is why I try to avoid is
where possible.
flatMap
as filter
In most cases, we can avoid is
by explicitly using instanceof
or !
. In the first example, we can just do:
const p: Pen | undefined | null = getPen()
if (p instanceof Pen) {
// typescript knows that `p` is `Pen`
} else {
// typescript knows that `p` is `undefined | null`
}
// or
if (!!p) {
// typescript knows that `p` is `Pen`
} else {
// typescript knows that `p` is `undefined | null`
}
However, when using filter
, is
is basically inevitable.
I have therefore come up with this technique of using flatMap
as filter
.
const ps: (Pen | undefined | null)[] = getPens()
const pens = ps.flatMap(p => !p ? [] : [p])
// typescript knows that `pens` is `Pen[]`
Since typescript can infer the p => !p ? [] : [p]
as Pen | undefined | null => Pen
, the resulting array will be Pen[]
.
With this technique, we are relying on typescript's type inference instead of manual type guarding (is
) to filter the list. This gives developers much more confidence about the type. I personally also think that it is worth the extra noise added to the code for the type safety. (Let me know in comments if you don't!)
If you are confused about how flatMap
works, you can read the following short introduction on flatMap
.
flatMap
?flatMap
can basically be defined as (roughly):
const flatMap = <X, Y>(xs: X[], fn: X => Y[]): Y[] => {
return xs.map(fn).flat()
}
In English, it maps each item x
of xs
into an array ys
and flattens it.
Below is a illustration of how flatMap
works
[1, 2, 3].flatMap(x => [x, x, x])
// under the hood
[1, 2, 3]
⬇
⬇ .map(x => [x, x, x])
⬇
[[1, 1, 1], [2, 2, 2], [3, 3, 3]]
⬇
⬇ .flat()
⬇
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Type safety is the reason why we went for Typescript. So, using is
and as
feels like it kind of defeats the purpose. Typescript's type system help catch many problems. But is
and as
bypasses that and make problems undetectable. By avoiding is
and as
, developers will have more confidence in their code.
What do you think about this technique? Let me know in the comments!