How to `filter` without `is` in Typescript? the `flatMap` as `filter` technique
Tags: typescript
Published: 2020-02-25 15:06:17

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*.

```ts
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](https://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=17&pc=2#code/CYUwxgNghgTiAEkoGdnwAogHbwN7wF8AoUJOeAMwFcswAXASwHscBzEOzLACgEoAuDNngAfeDVAUGWEMFHwsVCBCKqwLZHXgNkXeAF543AB6CoWAJ4D4x7Wj36AfHiLx4cOlRg5b0zebAQJgohLCJiInUsTXgAB0E9MQkQKRk5MUVlA3h2Tmw+VQYQ7h0ubljeXhc3AHoa+DoLWJBkMBgGWK0AaywmAHc0OgALKC0AA1ixu3gxrjHXOPD4EAhkBFwFuoamlraO7t6BhpHxyemx5NTZeUyIebdY8KA). 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`.

```ts
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.

```ts
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:

```ts
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`.

```ts
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):

```ts
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!

[type guard]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types