TypeScript first appeared in October 2012, and it was one of those tools that at first I didn’t like. Mapped types first version arrived in TypeScript 2.1, which was released nearly four years after the first release.
Looking at Google Trends, we can see how interest started to grow in 2015 (Visual Studio Code release date) and then really took off in 2016. Mapped types and Angular 2 were released in 2016, which should account for that growth.
What are mapped types anyway? They are just a way to avoid defining interfaces over and over. You can base the type you need on another type or another interface and save yourself a lot of manual work.
You can use the TypeScript Playground app below to run all the snippets included in this article. It’s definitely a playful tool where you can practice and improve your TypeScript skills.
Let’s start seeing some TypeScript in action.
interface Point {
x: number;
y: number;
}
If you want to extend this interface, you can do the following.
interface Point3D extends Point {
z: number;
}
But you can use mapped types as well:
type Point3D = Point & { z: number };
Cool, isn’t it? Although in this scenario we are not really showing anything that can’t be done with interfaces, are we? We are just getting started, though.
That being said, it’s preferable to use interfaces in this particular scenario. This is just a showcase so you can see its versatility.
You can easily combine two types, as follows:
interface A {
a: string;
}
interface B {
b: string;
}
type AB = A & B;
Here is where it really starts to shine. We can see how easy to do and readable this becomes.
Before diving a bit deeper, let’s look at the
keyof
primitive introduced in version 2.1. It looks scary at first, but it’s super intuitive: It will just expose the keys of any given interface/type through a union. Those are known as Typescript Union Types.interface Book {
author: string;
numPages: number;
price: number;
}
type BookKeys = keyof Book; // 'author' | 'numPages' | 'price';
Built-In Utility Types
TypeScript does ship with a lot of utility types, so you don’t have to rewrite those in each project. Let’s look at some of the most common:
interface Book {
author: string | null;
numPages: number;
price: number;
}
// Article is a Book without a Page
type Article = Omit<Book, 'numPages'>;
// When it's using functional programming to use immutable objects
type BookModel = Readonly<Book>;
const book: BookModel = { author: 'Jose Granja', numPages: 2, price: 0 };
book.author = 'Robert Smith' // this will raise an error since all the properties are readonly
Omit, Partial, Readonly, Exclude, Extract, NonNullable, ReturnType
— those are the most common.You can check out all of them in more detail here.
You are not limited to using only one at a time but can combine them as much as you need, and this is where the fun really starts.
type AuthoredBook = Omit<Book, 'author'>
& NonNullable<Pick<Book,'author'>>;
// this result in a type with a "author: string" type. Author is not nullable anymore
Build Your Own Utility Types
Let’s look first at how TypeScript does its utilities. We’ve used Omit, which was introduced in TypeScript 3.5. It’s a combination of two other utilities.
Before starting, let’s cover how to access interface/type properties.
// definition of Omit
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
//definition of Exclude
type Exclude<T, U> = T extends U ? never : T;
//definition of Pick
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
Pretty easy when you break it down into small units, isn’t it?
keyof
does play a big role here since it will be making sure that the properties we wanted to omit to belong to the type/interface we want to be omitted.Let’s create our first custom utility. Let’s say we want to define an object where all key values are from a particular type.
type GenericString = { [key: string]: string }
Pretty easy, right? Let’s use Generics and make the string type configurable.
type GenericObject<T> = { [key: string]: T }
Let’s make that key type configurable:
type GenericKeyObject<K extends keyof any, T> = {
[P in K]?: T;
};
Let’s see that in action.
type BooleanList = GenericKeyObject<string, boolean>;
const countriesVisited: BooleanList = {
France: true,
Italy: true
};
// let's take it a step further
type VisitableCountries = 'Hungary' | 'France' | 'Germany';
type CountriesChecklist = GenericKeyObject<VisitableCountries, boolean>;
const countriesVisitedCurrentYear: CountriesChecklist = {
France: true,
Hungary: true
};
const countriesVisitedLastYear: CountriesChecklist = {
France: true,
Belgium: true
}; // this fails at compile time since Belgium is no in the list of visitable countries.
What works for me when building complex mapped types is starting simple and then incrementally adding complexity. You might find that starting too complex is frustrating.
Level-Up Your Utility Types and Usage
It’s easy to build a very simple and trivial example with mapped types. Let’s now dig a bit deeper.
Let’s check out the never primitive first, though. It was introduced in version 2.0. It indicates that the value will never occur.
interface HttpResponse<T, V> {
data: T;
included: V;
}
// let's use never
type StringHttpResponse = HttpResponse<string, never>;
// What you are here doing is banning StringHttpResponse consumers from using the included property as even in other usages of HttpResponse it might be populated it is not in this particular response. You are communicating that this property must be ignored
Let’s now check out the
infer
primitive added on version 2.8.As you know, TypeScript relies heavily on type inference. You can refresh your memory here.
What this
infer
primitive does is it empowers your mapped types with inference. Now you can extract and infer a type inside a conditional type.What is a conditional type? It’s just an expression for selecting one of two possible types based on a condition expressed as a type relationship.
T extends U ? X : Y
Let’s put all that into play.
interface ResponseData {
data: string[];
hasMoreItems: boolean;
}
const getData = (): Promise<ResponseData> => {
const data = {
data: ['one', 'two', 'three', 'four'],
hasMoreItems: false
}
return Promise.resolve(data);
}
What if you wanted to unpack the type from the
Promise
returned by getData
? Simple: Create your own utility using infer
.type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U :
T;
type PromiseResult = Unpacked<ReturnType<typeof getData>>;
// PromiseResult = ResponseData
type FunctionResult = Unpacked<() => string>;
// FunctionResult = string
What’s happening there? You are just inferring the type if T extends a
Promise
or a Function
; otherwise, you are just returning the given type T
.Let’s do one last one as a bonus. Let’s do an interface that will return
true
if the object is empty and false
if the type has some properties. Yes, we can do crazy stuff like that in TypeScript.type Empty<T extends {}> = {} extends Required<T> ? true : false;
type isEmpty = Empty<{}>; // true
type isNotEmpty = Empty<{ name: string}>; // false
Why is that useful? You can combine the mapped type Empty with any conditional infer/mapped type to create more custom types. So we just created a tool that will help us create more mapped types.
type HttpDataReponse<T> = Empty<T> extends true
? never
: T;
type ClassReponse = HttpDataReponse<{}>; // result be never
type BookReponse = HttpDataReponse<{ author: string}>; // result be { author: string }
Pretty crazy what you can achieve, isn’t it? The limit is your imagination.
TypeScript Tuples
One of the last additions and improvements was Tuples. Tuples are arrays where the number of elements is fixed. Their type is known and they are not necessarily the same type.
let arrayOptions: [string, boolean, boolean];
arrayOptions = ['config', true, true]; // works
arrayOptions = [true, 'config', true]; // does not work
function printConfig(data: string) {
console.log(data);
}
printConfig(arrayOptions[0]);
Tuple
is very useful. There’s one scenario where it’s very common to find it nowadays: React Hooks. The Hooks implementation normally returns an array with the result plus functions, and having that array typed is indeed very handy.Wrap Up
And just like that, we are done. We started with some basic mapping and ended up with some Typescript advanced types. Mapped types are by far the most fun feature in TypeScript.
They serve as a good showcase of how powerful and dynamic TypeScript can be. If you were hesitant to try TypeScript, I hope my article gave you that very last push you needed. Even if you are not planning to use it now, it is wise to know what’s out there in case it comes in handy in the near future.
For those heavy TypeScript users, I hope you can agree on how much fun mapped types are once you get the hang of them.
More TypeScript content will be coming in the future — Cheers!
Also published at https://medium.com/better-programming/mastering-typescripts-mapped-types-5fa5700385eb