YCM Jason

How to `filter` without `is` in Typescript? the `flatMap` as `filter` technique

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.

# What is is?

is is called a type guard in Typescript. It hints typescript about the type of an argument of a type predicate.

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. You can hover on the variables to see how the type narrows down in the if statements.

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.

# What is 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]

# Conclusion

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!