DEV Community

Cover image for Byte sized TypeScript #1 - Filter type
Anurag Hazra
Anurag Hazra

Posted on

Byte sized TypeScript #1 - Filter type

Hiya! Welcome to the first post of my new TypeScript blogpost series.

In this series I'll demonstrate & deconstruct byte sized TypeScript snippets to help you understand how to write type-level code.


Let's start shall we?

Filter Type

In TypeScript we have a notion of tuple types which are basically directly comparable to Arrays in js. It looks something like this:

type MyTuple = [1, 2, string, boolean, null];
Enter fullscreen mode Exit fullscreen mode

Now let's say we want to implement a Filter method similar to JavaScript's native Array.Filter in Type Level, how do we do this?

Here's the full implementation:
As we go forward I'll explain this type line by line.

type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement,
  ...infer Rest
]
  ? FirstElement extends FilterBy
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>
  : Arr;

type Demo = Filter<[1, 2, string, boolean], number>
// -> [1, 2]
Enter fullscreen mode Exit fullscreen mode

Open on playground

Line 1:

In line 1 we have Arr extends [ infer FirstElement, ...infer Rest]

In here we are saying:
If Arr is equal to [infer FirstElement, ...infer Rest] then go into the true branch of the conditional type. This line will also ensure that the passed Arr is actually a Tuple. If not, the false branch will be taken which is returning the Arr in the last line.

The important part is the infer keyword, infer keyword is the inspection glass of TypeScript, it can extract what the value of the type is at a particular place.

Basically we are asking the TypeScript compiler "Hey! whatever is in this place assign it to this variable called FirstElement"

Then we can access this FirstElement type's value in the conditional branch.

Let's see an example:

type GetFirstElementOfArray<Arr extends any[]> = 
  Arr extends [infer FirstElement, ...infer Rest] 
    ? FirstElement 
    : never

type Demo = GetFirstElementOfArray<[1, 2, 3]> // 1
Enter fullscreen mode Exit fullscreen mode

You'll also notice the ...infer Rest part, It is similar to JavaScript's rest syntax. This part will infer the rest of the elements in the array and save it in the variable called Rest which you can also access in the conditional branch, in this case it will have the values [2, 3].

Line 5, 6, 7:

Now we are inside the true branch of the conditional type

Screenshot of the Filter type with highlighted lines 5,6 & 7

At FirstElement extends FilterBy we are checking if the FirstElement is equal to FilterBy

If the above condition passes we are going to return a new tuple type [FirstElement, ...Filter<Rest, FilterBy>], here we are including the FirstElement & then recursively calling the Filter generic again with the Rest of the elements.

Notice that we are also using the spread syntax ...Filter<Rest, FilterBy>, since the Filter generic returns a tuple we can spread it's value in this new tuple we are returning.

And if FirstElement extends FilterBy fails we are going to call the Filter recursively again but skipping the FirstElement this time, Filter<Rest, FilterBy>.

Now let's step through the code on each recursive call to understand better what's happening:

Filter type step by step recursion states visualized

// Initial state
type D = Filter<[1, 'hello', 'world'], number>
Enter fullscreen mode Exit fullscreen mode

Recursion #1:

// input <[1, 'hello', 'world'], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // 1
  ...infer Rest // ['hello', 'world']
]
  ? FirstElement extends FilterBy // 1 == number -> true
    ? [FirstElement, ...Filter<Rest, FilterBy>] // [1, ...Filter<['hello', 'world'], FilterBy>]
    : Filter<Rest, FilterBy>
  : Arr;

Enter fullscreen mode Exit fullscreen mode

Recursion #2:

// input <['hello', 'world'], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // 'hello'
  ...infer Rest // ['world']
]
  ? FirstElement extends FilterBy // 'hello' == number -> false
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>  // Filter<['world'], FilterBy>
  : Arr;
Enter fullscreen mode Exit fullscreen mode

Recursion #3:

// input <['world'], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // 'world'
  ...infer Rest // []
]
  ? FirstElement extends FilterBy // 'world' == number -> false
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>  // Filter<[], FilterBy>
  : Arr;
Enter fullscreen mode Exit fullscreen mode

Recursion #4:

// input <[], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // unknown
  ...infer Rest // unknown
]
  ? FirstElement extends FilterBy
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>
  : Arr;
  // ^ false branch is taken since `Arr extends [infer FirstElement, ...infer Rest]` failed,
 //   thus empty Arr is returned.
Enter fullscreen mode Exit fullscreen mode

That's about it, I hope you understood how the Filter type works now. At it's core the type is simple enough but understanding how each piece works is the important part.

In case you still couldn't understand the type level code, how about we rewrite the type level code to runtime javascript code?

You'll be surprised how similar it looks:

function Filter(Arr, FilterBy) {
    if (Arr.length < 1) return Arr;
    const [FirstElement, ...Rest] = Arr;
    if (typeof FirstElement === FilterBy) {
        return [FirstElement, ...Filter(Rest, FilterBy)];
    } else {
        return Filter(Rest, FilterBy)
    }
}

Filter([1, 2, 3, 'hello', true], 'number')
// [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Cool isn't it?

Anyways I think that's about it, I hope you learned something new & enjoyed the post.

I'll be continuing this blogpost series, so make sure to follow me on twitter or dev.to to get updated.

Top comments (6)

Collapse
 
warwait profile image
Parker Waiters

Really good article.

Collapse
 
maxfindel profile image
Max F. Findel

Excellent explanation Anurag, thank you. I think the examples and the explanation helps me understand the typescript syntax a bit better and get used to it. Sometimes I find myself fighting with the VS Code type checker because I screw up the syntax!

Just as you describe in your last example, the same function looks very similar on plain JS. The ... function, decomposition and recursion are the ones doing all the magic here :)

Collapse
 
anuraghazra profile image
Anurag Hazra

Yes of course, once you get the mental model of type level programming it becomes a second nature. I'm glad you understood the post.

Collapse
 
rzs401 profile image
Richard Smith

Thanks for writing

Collapse
 
sirrocco profile image
RC

Very nice article, thanks. Would be useful if you add one or 2 examples of when one might use this.

Collapse
 
anuraghazra profile image
Anurag Hazra

Yeah sure, I was writing a typesafe version of the classnames package where I needed to filter out never & boolean types github.com/anuraghazra/type-triden...