Learn the Key Concepts of TypeScript’s Powerful Generic and Mapped Types
author
Matías Hernández
Published
2 years ago
As you write more complex types, you'll notice your code growing.
You might also start seeing code duplication…
When you need to reuse TypeScript code, what do you do? How can you keep your code DRY? How do you reduce the boilerplate code?
There are a few main concepts in Typescript that help you accomplish that goal of reducing boilerplate and reusing more code.
We’ll be specifically be covering
Generics
and
Mapped Types
Both concepts can be a bit scary at first, mostly because of a knowledge gap that creates a wall and makes the code un-readable at first.
Generics
are a way to create, in the type world, a similar functionality that functions offer.
Mapped Types
are a way to derive and reshape types into new ones.
But, before diving into these new concepts, you need to build up your knowledge with the key features that enable the power of Generics and Mapped Types.
The keyof operator
keyof
is Typescript's answer to JavaScript’s
Object.keys
operator.
Object.keys
returns a list of an object’s keys.
keyof
does something similar but in the typed world only. It will return a literal union type listing the "properties" of an object-like type.
This operator is the base building block for advanced typing like mapped and conditional types.
The keyof operator takes an object type and produces a string or numeric literal union of its keys. - Typescript Handbook
typeColors={primary:'#eee',primaryBorder:'#444',secondary:'#007bff',black:'#000',white:'#fff',whiteBorder:'#f2f2f7',green:'#53C497',darkGreen:'#43A17C',infoGreen:'#23AEB7',pastelLightGreen:'#F3FEFF',}typeColorKeys=keyofColors;// "primary" | "primaryBorder" | "secondary" ....functionSomeComponent({ color }:{color:ColorKeys}){return"Something"}SomeComponent({color:"WhateverColor"})SomeComponent({color:"primary"})
The code sample above is from a real webapp. The
Colors
type describes a set of colors that can be used across the application.
The
keyof
operator retrieves a
literal union
of all the possible colors from the
Colors
type.
Literal union
means a Union type made up by literal values like "primary" | "primaryBorder"
The union is then used to type the props of
SomeComponent
, allowing the
color
argument to be one of the colors defined in the type.
keyof
as constraint
The
keyof
operator can also be used to apply constraints to a generic.
For this example, it is enough to know that a Generic behaves similarly to a function argument and that the Generic type can have a type or constraint.
The “Generic” code is the odd angle brackets section
<Obj, Key extends keyof Obj>
. That section defines that this function will receive two
"type parameters"
that obey certain rules.
Then, the function declares that the arguments are of the type of that
"type parameters"
and it will return a type derived from the generic values as
Obj[Key]
Let's dissect the generic portion of the above function definition:
Obj
is the name used to identify this Generic parameter. Usually people use single letters to identify the generic, but IMHO it is more clear to use a better name like you would with a variable. The intention here is to accept any object.
The second generic parameter is
Key extends keyof Obj
. Here the
extends
keyword is used as a constraint and can be read as "Key is of ...." meaning that the
Key
generic can only be a value found in
keyof Obj
.
keyof Obj
is, as mentioned before, a union of string literals from the properties of
Obj
and
Obj
is the first generic parameter. So in Typescript you can reference the previous generic directly.
So, does all of that mean?
It means that the function arguments of
getObjectProps
will accept any object inside the
obj
argument, but they
key
argument
can only be a string literal that exists as a property of
obj
const User ={name:'Matias',site:'matiashernandez.dev',location:{country:'Chile',city:'Talca'}}const email =getObjectProps(User,'email') //Argument of type '"email"' is not assignable to parameter of type '"name" | "site" | "location"'.const loc =getObjectProps(User,'location')
Other type of constraint that you can write using the
keyof
operator is to restrict the return type of a function.
functionobjectKeys<ObjextendsRecord<string,unknown>>(obj:Obj): (keyofObj)[] {returnObject.keys(obj) as (keyofObj)[];}
The above example can be read as
objectKeys
accepts a
obj
arguments that has to be of type
Record<string, unknown>
and will return an array of all of the properties of that obj.
keyof
and template literals.
You can also use
keyof
to construct complex template literals like the following example.
This will generate a union of literal strings based on the
State
properties concatenating the property name with the
set
word.
As you can see, the
keyof
operator can be small but it is an important piece that unlocks powerful operations when used in the right places.
The
extends
keyword
This keyword is very confusing at first glance. It's used in a few different places with different meanings.
The first usage of
extends
is for interface inheritance. This lets you create new interfaces that inherit the behavior of a previous one. In other words, it extends the base interface/class.
The first interface,
User
, describes a type with a few properties representing a general user of a certain application.
The second interface, named
StaffUser
, represents a user who is part of the organization and therefore has a set of
roles
. But this user also has
firstName
,
lastName
, and
email
.
You don't want to write that over and over again, right?
But more important, what if the general
User
entity changes? How can you be sure that the change is represented everywhere else?
That's why the
StaffUser
is written with the
extends
keyword, saying that this interface has all of the properties of the
User
interface, plus the ones defined by itself.
This usage of
extends
can be used to inherit from multiple interfaces at the same time by using a comma-separated list of the base interfaces.
interfaceStaffUserextendsUser,RoleUser{// ...}
This behavior can also be used to extends a Class
Another usage of the
extends
keyword is to
narrow the scope
of a generic to make it more useful.
This usage of
extends
narrowing down or constraining the type of a generic
is the corner stone to be able to implement conditional types since the
extends
will be used as condition to check if a generic is or not of a certain type.
In the example above, from a real world implementation of
tanstack-query
, a new type is defined:
QueryFunction
. This type accepts two generic values,
T
and
TQueryKey
.
The
extends
keyword here
constrains
the possible values of the
TQueryKey
generic to be of the type
QueryKey
, defined elsewhere in the source code. In other words,
TQueryKey
has to be of type
QueryKey
.
If you're not yet comfortable with the use of generics, think of them as function arguments in the type world. The
QueryFunction
type can be thought of as a function type that accepts two arguments (generics) named
T
, with a default value of
unknown
, and
TQueryKey
, with a default value of
QueryKey
.
This usage of
extend
, narrowing down or constraining the type of a generic, is the cornerstone of being able to implement conditional types, since
extends
is used as a condition to check whether a generic is or is not of a certain type.
The
never
keyword.
never
is a type “value” that represents something that will never occur. This is very handy for implementing different types as conditionals and discriminated unions.
A simple example can be defining the type of a set of function arguments, where some of those arguments are dependent:
A React component that can accept some props is a good example. In the above code, you can see that there are three types defined:
CardWithDescription
defines a type that can have a
description
property as
string
, but with
title
and
footer
as
never
, meaning that they cannot be defined or used.
CardWithoutDescription
is the opposite type, where
description
cannot be used, but
title
and
footer
are mandatory.
CardProps
defines a union of the previous two which is used to type the props of the
Card
component.
With this setup, the
Card
component can only be used with
description
and no
footer
and no
title
, or vice versa. If for some reason you try to use the three props together, you'll get the following error:
Type '{ description: string; title: string; footer: string; }' is not assignable to type 'IntrinsicAttributes & CardProps'.
Type '{ description: string; title: string; footer: string; }' is not assignable to type 'CardWithoutDescription'.
Types of property 'description' are incompatible.
Type 'string' is not assignable to type 'undefined'.
Generics
If you're serious about becoming a true expert and taking your career to the next level, check out Matt Pocock's Total TypeScript and learn the underlying principles and patterns of being an effective TypeScript engineer.
In any programming language you have ways to implement the DRY principle, TypeScript is no different.
Generics help you build well-defined and consistent APIs that are also reusable. You can use Generics to build dynamic and reusable pieces of code that resemble JavaScript functions.
Let's see an example of a generic in the wild.
typeIsArray<T>=Textendsany[] ? true : false;typeres1=IsArray<number[]>;typeres2=IsArray<["a","b","c"]>;typeres3=IsArray<"this is not an array">
The example shows a type called isArray. It receives a Generic "parameter" called T and uses that to do some conditional "calculation" to return true or false.
This example can give you a hint: Generics are kind of like the function parameters of the typed world.
For convention, the name of the Generics is a single capital letter, but that is not required. You can pass any name to it.
The interface UserInfo has two properties that depend on the generics used:
· name with a type of X
· rol with a type of Y
So when you use UserInfo<string, string>, both properties will be strings.
You'll find generics in almost every TypeScript code-base. They're the basic way to create reusable chunks of code and also the basic tool that library authors use to create a flexible API.