TypeScript Fundamentals: A Deep Dive into 'readonly,' 'extends,' and 'as const

We are now on day number 5. Christmas is closer, Santa is getting a little bit more tired and the TypeScript topics are getting spicier. Today's challenge will push us to dive into three new typescript concepts, which will enforce our personal TS arsenal.

I think now we are moving from beginner to more intermediate concepts, which is exciting, let's dive into the challenge!

Day five - Dec 5th

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

Challenge:

Organize Santa's List

The challenge

It's been a tough year for Santa's workshop. The elves are a little behind schedule on getting Santa his list. Santa really really likes to see the full list of names far in advance of Christmas Eve when he makes his deliveries.

Normally the elves get lists like this

const badList = ["Tommy", "Trash", "Queen Blattaria", /* ... many more ... */];
const goodList = ["Jon", "David", "Captain Spectacular", /* ... many more ... */];

And they copy-pasta all the values into a TypeScript type to provide to Santa like this

type SantasList = [
  "Tommy", "Trash", "Queen Blattaria", /* ... many more ... */
  "Jon", "David", "Captain Spectacular", /* ... many more ... */
];

But there's a problem.. There's one elf on the team, Frymagen, that constantly reminds the others how incredible his Vim skills are. So he has always done it in years past. However this year, Frymagen got one of those MacBook Pros without the escape key and his Vim speed is drastically reduced. We need to find a better way to get Santa his list.

Let's implement SantasList such that it can be passed the types for the badList and goodList and it will return a TypeScript tuple with the values of both lists combined.

The code to complete

type SantasList = unknown;

The tests

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

const bads = ['tommy', 'trash'] as const;
const goods = ['bash', 'tru'] as const;

type test_0_actual = SantasList<typeof bads, typeof goods>;
type test_0_expected = ['tommy', 'trash', 'bash', 'tru'];
type test_0 = Expect<Equal<test_0_actual, test_0_expected>>;

type test_4_actual = SantasList<['1', 2, '3'], [false, boolean, '4', ['nested']]>;
type test_4_expected = ['1', 2, '3', false, boolean, '4', ['nested']];
type test_4 = Expect<Equal<test_4_actual, test_4_expected>>;

The solution

type SantasList<
  B extends readonly any[], G extends readonly any[]
> = [...B, ...G];

I know I know, this type definition looks very confusing and verbose at first, so let's break down what is going on here.

We are defining the SantasList type as a generic type that takes two parameters B and G. Both parameters are expected to be readonly arrays (I'll explain why this is important in a bit). The type is constructed using the spread (...) operator to concatenate the elements of the two arrays:

  • B extends readonly any[]: This constrains the type parameter B to be a readonly array.

  • G extends readonly any[]: This constrains the type parameter G to be a readonly array.

  • [...B, ...G]: This uses the spread operator to create a new array that includes all the elements of B followed by all the elements of G.

You may notice that we are using the keyword extends, in TypeScript it is used to specify constraints on a type parameter. In this context, it indicates that B must conform to a certain structure or set of constraints.
In this case the type B can only be a read-only array of any (readonly any[] ).

But why are we using readonly? Usually, this choice is made for two reasons:

  1. Immutability: When an array is readonly it provides immutability. Once an array is declared as readonly, its elements cannot be modified. This aligns with functional programming principles that encourage immutability, making it easier to reason about and preventing unintended side effects.

  2. Type Inference: If an array is declared as readonly, TypeScript can provide better type checking and catch potential errors where code attempts to modify the array.

In this case, we need to include it in the type because of the nature of the test inputs (bads and goods):

const bads = ['tommy', 'trash'] as const;
const goods = ['bash', 'tru'] as const;

In TypeScript, when you use as const with an array literal, it creates a readonly array.

When you use the as const assertion on a variable, TypeScript treats the variable as having the narrowest (most specific) possible type.
As a consequence, its type will be treated as a readonly.

Summary

In this challenge, we learned about a couple of TypeScript fundamentals:

  • readonly keyword

  • as const assertion

  • extends keyword

Had a lot of fun with today's challenge! Hope you enjoyed the prompt and the breakdown. We're inching closer to Christmas—exciting times! 🎄✨