New Features in TypeScript 4.1
I’ve always loved TypeScript and the language just keeps getting better with each evolution. 4.1 has lots of interesting new features. Here I'm going to look at:
- Template Literal Types
- Key Remapping in Mapped Types
- Recursive Conditional Types
Template Literal Types
TypeScript has supported string literals types since 1.8.
type Beatles = "John" | "Paul" | "George" | "Ringo"
These are extremely powerful when we want to bring safety to stringly typed API’s. Literal types were later extended to support numeric, boolean and enum literals as well.
type StoryPoints = 1 | 2 | 3 | 5 | 8 | 13 | 21 | 'Infinity';
In 4.1, TypeScript now supports Template Literals as types. Where normal template literals embed expressions within a string, Template Literal Types allow types to be embedded.
type Suit =
"Hearts" | "Diamonds" |
"Clubs" | "Spades";
type Rank =
"Ace" | "Two" | "Three" | "Four" | "Five" |
"Six" | "Seven" | "Eight" | "Nine" | "Ten" |
"Jack" | "Queen" | "King";
type Card = `${Rank} of ${Suit}`;
const validCard: Card = "Three of Hearts";
const invalidCard: Card = "Three of Heart"; // Compiler Error
The Card
type will be every permutation of the Suit
and Rank
in the template
${Rank} of ${Suit}
. As you can see this is useful for building out patterns of string types. It can
also be useful when combining with other type operators such as keyof
.
interface UserData {
name: string;
age: number;
registered: boolean;
}
// Generates: "getName" | "getAge" | "getRegistered"
type UserDataAccessorNames = `get${Capitalize<keyof UserData>}`;
As you can see the string type has been capitalized - TypeScript 4.1 has added 4 such helpers,
Capitalize<T>
Uncapitalize<T>
Uppercase<T>
Lowercase<T>
Key Remapping in Mapped Types
We can build upon the template literal types feature by combining with mapped-types. If we think about generating a type for the accessors in the previous example we can start with a mapped type:
type UserDataAccessors = {
[K in keyof UserData]: () => UserDataAccessors[K];
}
This creates a new type with the original data members mapped to a function returning their type:
{
name: () => string;
age: () => number;
registered: () => boolean;
}
Now we need to map the keys of the type to `get${Capitalize<keyof UserData>}`
. We could do
this simply as [K in `get${Capitalize<keyof UserData>}`]
but we need to retain the
original K
so we access UserDataAccessors[K]
. Remapping with as
allows us to map the
left-hand side while still having access to the original key:
type UserDataAccessors = {
[K in keyof UserData as `get${Capitalize<K>}`]: () => UserData[K];
}
This creates the type:
{
getName: () => string;
getAge: () => number;
getRegistered: () => boolean;
}
Recursive Conditional Types
The final 4.1 feature I'm going to talk about in this post revolves around everyone's favourite software topic, Recursion.
Recursive Conditional Types are exactly what the name suggests, Conditional Types that reference themselves.
type BuildTuple<Current extends [...T[]], T, Count extends number> =
Current["length"] extends Count
? Current
: BuildTuple<[T, ...Current], T, Count>
type Tuple<T, Count extends number> = BuildTuple<[], T, Count>
This example creates a Tuple
type that will generate a Tuple of a specified size. For example:
// Generates: [string, string, string, string, string]
type StringQuintuple = Tuple<string, 5>
Let's break down what's happening in this example.
type BuildTuple<Current extends [...T[]], T, Count extends number> =
We can think of this type like a recursive function with the type parameters the parameters of
the function. The key thing to remember here is that we are dealing with types, not values.
The Current
parameter is the tuple as it is being built up, T
is the type of every element
in the tuple and Count
is the required number of entries. Note the way Current
is constrained
as a variadic tuple type.
type BuildTuple<Current extends [...T[]], T, Count extends number> =
Current["length"] extends Count
? Current
: BuildTuple<[T, ...Current], T, Count>
So if the Current
tuple is the correct 'length', we are finished and evaluate as Current
.
Otherwise we invoke the type again with an extended tuple. Again, note how the new Current
is
generated by using the spread operation on the previous Current
- [T, ...Current]
.
We already had recursive types in TypeScript but this version allows us to use them directly in conditional types.
Crazy Examples
If you'd like to see some fun examples of this, I've created a couple of repositories on GitHub. These examples 'compute' a type that is the solution to a problem. Note that the compiler will generate errors as there's too much recursion going on, but this is just a bit of mad fun.
Sudoku
https://github.com/eamonnboyle/sudoku-type-solver
Here we have code that generates a type that solves a Sudoku puzzle.
The example uses the string literal types "true"
and "false"
to create generic types
that act as pseudo conditional functions.
type FilterSet<C extends Numbers, F extends Numbers> =
IsSingleNumber<C> extends "true" ? C : Exclude<C, F>;
I haven't used the booleans true
and false
as the union true | false
gets simplified to boolean
and I want the distinct values.
The main solver type uses recursion:
export type SolveGame<G extends SudokuGame> =
IsGameComplete<G> extends "true"
? G
: SolveGame< SolverIteration<G> >;
12 Days of Christmas
https://github.com/eamonnboyle/12-days-of-christmas-type-solver
This project generates the lyrics of the classic holiday song, The 12 Days of Christmas.
It makes heavy use of string literals including template literal types:
const DaysTuple = [
'Twelfth', 'Eleventh', 'Tenth', 'Ninth', 'Eighth', 'Seventh',
'Sixth', 'Fifth', 'Fourth', 'Third', 'Second', 'First',
] as const;
type DaysTupleType = typeof DaysTuple
type Days = DaysTupleType[number];
type FirstLine<D extends Days> = `On the ${D} day of Christmas my true love sent to me,`
Note how the Days
union is created, using a
const
assertion.
This can sometimes be useful when you want to have a single source of truth for multiple entities such
as data, a tuple type and a union type.
The main solver again uses recursion:
export type TwelveDaysOfChristmas<
D extends readonly [...Days[]] = DaysTupleType,
G extends [...Gifts[]] = GiftsTupleType
> =
D["length"] extends 0
? []
: [
...TwelveDaysOfChristmas<Tail<D>, Tail<G>>,
DayVerse<D[0], GiftsForDay<D, G>>
]
Conclusion
I hope that this article gets you excited about TypeScript and some of its more advanced type features.
Checkout our TypeScript course. We're also happy to deliver
React training using TypeScript.
We deliver virtually to companies all over the world and are happy to customise
our courses to tailor to your team's level and specific needs.
Come and check us out to see if we can help you and your team.