Photo by Cofohint Esin on Unsplash
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:
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 parameterB
to be a readonly array.G extends readonly any[]
: This constrains the type parameterG
to be a readonly array.[...B, ...G]
: This uses the spread operator to create a new array that includes all the elements ofB
followed by all the elements ofG
.
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:
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.
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! 🎄✨