Filtering Types with Template Literals in TypeScript

We have officially completed a week of TypeScript challenges, woohoo! Let's continue the streak so we can keep growing our TypeScript fundamentals 💪🏻.

Today’s challenge is “short”, but it will still require to use of a lot of the recent TS fundamentals we have covered. Excited to start week 2 of challenges, let’s get started!

Day eight - Dec 8th

I strongly encourage you to try the first challenge on your own before reading the solution here.

Challenge: Filtering The Children (part 3)

The challenge

Yet again, Santa has made a request to change the children filtering code. This time he just sent an email to the entire engineering team (which is absolutely not the process, but since Santa is sometimes a bit difficult to communicate with, no one has yet had the courage to tell him). Here's the contents of the email.

From: kris.kringle@hohoholdings.com
To: engineering@hohoholdings.com
Subject: Code Changes Needed

Hello beloved team!

Looks like we need some changes to the code again!

1. there are sometimes naughty kids in the same list
2. turns out I don't actually need to see the nice children in the list, after all
3. my golf game ran late this morning.. so since the other two changes were quick to implement, I'm sure this will be just as fast, right?!

- Kris Kringle
  "at Santa's workshop, we value loyalty over all else"

Wow. What a pointless email. For once, calling a meeting would have been better.

Good thing we got some experience reading the tests because this email may as well have said "do work. thanks." (lol).

Off to the tests to see how this is actually supposed to work!

The code to complete

type RemoveNaughtyChildren = unknown;

The tests

import { Expect, Equal } from 'type-testing';

type SantasList = {
  naughty_tom: { address: '1 candy cane lane' };
  good_timmy: { address: '43 chocolate dr' };
  naughty_trash: { address: '637 starlight way' };
  naughty_candace: { address: '12 aurora' };
};
type test_wellBehaved_actual = RemoveNaughtyChildren<SantasList>;
type test_wellBehaved_expected = {
  good_timmy: { address: '43 chocolate dr' };
};
type test_wellBehaved = Expect<Equal<test_wellBehaved_expected, test_wellBehaved_actual>>;

The solution

For the solution, we will need to combine a couple of topics we have covered in the past, since RemoveNaughtyChildren needs to be generic, use a mapped type and conditional type as well using a template literal:

type RemoveNaughtyChildren<T> = {
  [K in keyof T as K extends `naughty_${string}` ? never : K]: T[K];
};

Now let’s break down the solution and try to understand what’s happening with this huge type definition 😅:

  • We define RemoveNaughtyChildren<T> as a generic type that takes an object type T.

  • The type is using mapped type with a conditional type.

  • [K in keyof T as K extends naughty_${string} ? never : K] iterates over all keys K of the input type T.

  • For each key K, it checks if K extends the pattern naughty_${string} (i.e., if the key starts with "naughty_").

  • If the key matches the pattern, it assigns never (a type that can never be used) to that key, removing it from the resulting type.

  • If the key doesn't match the pattern, it includes the key and its corresponding value in the resulting type.

as K

Something else that is happening is that we are using as K , which may seem odd since we are already defining K at the beginning.

We need to do this to explicitly specify the type of the mapped key variable K in the mapped type expression. This is done for clarity and also to satisfy the TypeScript compiler:

[K in keyof T as K extends `naughty_${string}` ? never : K]: T[K];

Here, K is the key variable used in the mapped type. The as K at the end serves as a type assertion, indicating that the result of the conditional type (K extends naughty_${string} ? never : K) should be considered of type K.

I know know, TypeScript can be hard to satisfy sometimes 🙄.

Template literal

Another thing that is worth mentioning is how we are using the template literal:

K extends `naughty_${string}`

To be honest I don’t think it is super intuitive how TypeScript checks if the given Key (K) contains is a string that contains the prefix naughty_.

Under the hood, TypeScript uses string literal types and template literal types to do this kind of check. Our type definition naughty_${string} represents a string literal type (you have to use the string keyword) that starts with "naughty_" followed by any string (${string} is just a placeholder).

TypeScript can then use this type in a conditional type to check if another string literal type extends it.

Here is a simple version of the same idea:

type IsNaughty<K extends string> = K extends `naughty_${string}` ? true : false;

type IsTomNaughty = IsNaughty<'naughty_tom'>;   // true
type IsTimmyNaughty = IsNaughty<'good_timmy'>;    // false
type IsCandaceNaughty = IsNaughty<'naughty_candace'>; // true

Summary

To solve today’s challenge we had to rely on various key concepts of TypeScript:

  • Generic, Mapped, and Conditional types

  • The ‘as’ keyword to explicitly set types

  • Deeper understanding of string and template literals

One day closer to Christmas! I can already start smelling the turkey 🦃 (yes, our family tradition does turkey for Christmas). Feels great closing the year upgrading your TypeScript game, doesn’t it? See you tomorrow in the next one!

If you like this content consider checking out what I post on Twitter/X 🐦