Hello! I write tutorials to help beginner programmers learn TypeScript. My tutorials might NOT be as useful for experienced programmers learning TypeScript.
Why target beginner programmers? As TypeScript is becoming popular, more beginner programmers will be learning it. However, I noticed that many existing tutorials are not so friendly for beginner programmers. This is my attempt to offer a friendlier alternative.
// Confused by generics code like this?function makePair<F extends number | string,S extends boolean | F>()
If you’re (1) new to TypeScript, (2) new to generics, and (3) struggling to understand generics, then you’re exactly like me when I was learning Java 13 years ago.
Like TypeScript, the Java programming language supports generics. When I was studying Java in college, I was a beginner programmer, and generics felt very difficult to me. So I gave up on understanding generics at the time and used them without knowing what I was doing. I didn’t understand generics until I got a full time job after college.
Maybe you’re like me from 13 years ago and have felt that TypeScript generics are too difficult. If so, this tutorial is for you! I’ll try to help you actually understand generics.
If you already understand generics, you won’t find anything new in this tutorial. However, you might know someone (maybe one of your Twitter followers) who’s struggling with generics. I’d appreciate it if you could share this article with them. You can click here to tweet this article.
The source code for this site is on GitHub:
makeState()
First, I created a function called makeState()
below. We’ll use this function to talk about generics.
function makeState() {let state: numberfunction getState() {return state}function setState(x: number) {state = x}return { getState, setState }}
When you run makeState()
, it returns two functions: getState()
and setState()
. You can use these functions to set and get the variable called state
.
Let’s try it out! What gets printed out to the console when you run the following code? Try to guess first, and then press the Run button.
const { getState, setState } = makeState()setState(1)console.log(getState())setState(2)console.log(getState())
It printed 1
, then 2
. Pretty simple, right?
If you’ve used React, you might have realized that makeState()
is similar to the useState()
hook.
Confused? Some people might be wondering: “Why do we have functions inside another function?” or “What’s the { getState, setState }
syntax?” If so, click here for more explanation.
Now, instead of numbers like 1
or 2
, what happens if we use a string like 'foo'
? Try to guess first, and then press the Compile button.
const { getState, setState } = makeState()// What happens if we use a string instead?setState('foo')console.log(getState())
It failed to compile because setState()
expects a number:
function makeState() {let state: numberfunction getState() {return state}// setState() expects a numberfunction setState(x: number) {state = x}return { getState, setState }}
To fix this, we can change the type of state
and x
from number
to string
:
function makeState() {// Change to stringlet state: stringfunction getState() {return state}// Accepts a stringfunction setState(x: string) {state = x}return { getState, setState }}
It’ll now work! Press Run .
const { getState, setState } = makeState()setState('foo')console.log(getState())
Now that we got the basics down, here’s a challenge for you:
Question: Can we modify makeState()
such that, it can create two different states: one that only allows numbers, and the other that only allows strings?
Here’s what I mean:
// We want to modify makeState() to support// creating two different states:// One that only allows numbers, and…const numState = makeState()numState.setState(1)console.log(numState.getState()) // 1// The other that only allows strings.const strState = makeState()strState.setState('foo')console.log(strState.getState()) // foo
Earlier, our first makeState()
created number-only states, and our second makeState()
created string-only states. However, it couldn’t create both number-only states and string-only states.
How can we modify makeState()
to achieve our goal?
Here’s the first attempt. Does this work?
function makeState() {let state: number | stringfunction getState() {return state}function setState(x: number | string) {state = x}return { getState, setState }}
This does NOT work. You’ll end up creating a state that allows both numbers and strings, which is not what we want. Instead, we want makeState()
to support creating two different states: one that allows only numbers, and the other that allows only strings.
// Doesn't work because the created state…const numAndStrState = makeState()// Allows both numbers…numAndStrState.setState(1)console.log(numAndStrState.getState())// And strings.numAndStrState.setState('foo')console.log(numAndStrState.getState())// This is NOT what we want. We want to create// a number-only state and a string-only state.
This is where generics come in. Take a look below:
function makeState<S>() {let state: Sfunction getState() {return state}function setState(x: S) {state = x}return { getState, setState }}
makeState()
is now defined as makeState<S>()
. You can think of <S>
as another thing that you have to pass in when you call the function. But instead of passing a value, you pass a type to it.
For example, you can pass the type number
as S
when you call makeState()
:
// It sets S as numbermakeState<number>()
Then, inside the function definition of makeState()
, S
will become number
:
// In the function definition of makeState()let state: S // <- numberfunction setState(x: S /* <- number */) {state = x}
Because state
will be number
and setState
will only take number
, it creates a number-only state.
// Creates a number-only stateconst numState = makeState<number>()numState.setState(1)console.log(numState.getState())// numState.setState('foo') will fail!
On the other hand, to create a string-only state, you can pass string
as S
when you call makeState()
:
// Creates a string-only stateconst strState = makeState<string>()strState.setState('foo')console.log(strState.getState())// strState.setState(1) will fail!
Note: We call makeState<S>()
a “generic function” because it’s literally generic—you have a choice to make it number-only or string-only. And you know it’s a generic function if it takes a type parameter.
makeState<S>()
is a generic functionfunction makeState<S>() {let state: Sfunction getState() {return state}function setState(x: S) {state = x}return { getState, setState }}
You might be wondering: Why did we name the type parameter as “S
”?
Answer: It could actually be any name, but usually people use the first letter of a word that describes what the type is representing. In this case, I chose “S
” because it’s describing the type of a “S”tate. The following names are also common:
T
(for “T”ype)E
(for “E”lement)K
(for “K”ey)V
(for “V”alue)But wait a minute: If you pass boolean
to S
, you can create a boolean-only state.
// Creates a boolean-only stateconst boolState = makeState<boolean>()boolState.setState(true)console.log(boolState.getState())
Maybe we might NOT want this to be allowed. Suppose that don’t want makeState()
to be able to create non-number or non-string states (like boolean
). How can we ensure this?
makeState()
fromThe solution: When you declare makeState()
, you change the type parameter <S>
to <S extends number | string>
. That’s the only change you need to make.
function makeState<S extends number | string>()
By doing this, when you call makeState()
, you’d only be able to pass number
, string
, or any other type that extends either number
or string
as S
.
Let’s see what happens now when you try to pass boolean
as S
. Press Compile below.
function makeState<S extends number | string>() {let state: Sfunction getState() {return state}function setState(x: S) {state = x}return { getState, setState }}// What happens if we now pass boolean to S?const boolState = makeState<boolean>()
It resulted in an error, which is what we want! We have successfully prevented makeState()
from creating non-number or non-string states.
As you just saw, you can specify what’s allowed for the type parameter(s) of a generic function.
It can be annoying to specify types like <number>
or <string>
every time you call makeState()
.
So here’s an idea: Can we make it so that <number>
is thedefault type parameter of makeState()
? We want to make it so that, if S
is unspecified, it’s set as number
by default.
// Can we make it so that, <number> is the// default type paramter of makeState()?// We want these two statements to be equivalentconst numState1 = makeState()const numState2 = makeState<number>()
To make this happen, we can specify the default type of S
by adding = number
at the end. It’s kind of like setting default values for regular function parameters, right?
// Set the default type of S as numberfunction makeState<S extends number | string = number>()
By doing this, you can create a number-only state without specifying the type:
// Don’t need to use <number>const numState = makeState()numState.setState(1)console.log(numState.getState())
We are about two-thirds of the way through this article. Before we continue, let’s do a quick recap.
What you should remember is that generics are just like regular function parameters. The difference is that regular function parameters deal with values, but generics deal with type parameters.
Example 1: For example, here’s a regular function that takes any value:
// Declare a regular functionfunction regularFunc(x: any) {// You can use x here}// Call it: x will be 1regularFunc(1)
Similarly, you can declare a generic function with a type parameter:
// Declare a generic functionfunction genericFunc<T>() {// You can use T here}// Call it: T will be numbergenericFunc<number>()
Example 2: In regular functions, you can specify the type of a parameter like this:
// Specify x to be numberfunction regularFunc(x: number)// SuccessregularFunc(1)// ErrorregularFunc('foo')
Similarly, you can specify what’s allowed for the type parameter of a generic function:
// Limits the type of Tfunction genericFunc<T extends number>()// SuccessgenericFunc<number>()// ErrorgenericFunc<string>()
Example 3: In regular functions, you can specify the default value of a parameter like this:
// Set the default value of xfunction regularFunc(x = 2)// x will be 2 inside the functionregularFunc()
Similarly, you can specify the default type for a generic function:
// Set the default type of Tfunction genericFunc<T = number>()// T will be number inside the functiongenericFunc()
Generics are not scary. They’re like regular function parameters, but instead of values, it deals with types. If you understood this much, you’re good to go!
makePair
Let’s take a look at the new function called makePair()
. It’s similar to makeState()
, but instead of storing a single value, this one stores a pair of values as { first: ?, second: ? }
. Right now, it only supports numbers.
function makePair() {// Stores a pair of valueslet pair: { first: number; second: number }function getPair() {return pair}// Stores x as first and y as secondfunction setPair(x: number, y: number) {pair = {first: x,second: y}}return { getPair, setPair }}
Let’s try it out! What gets printed out to the console when you run the following code? Try to guess first, and then press the Run button.
const { getPair, setPair } = makePair()setPair(1, 2)console.log(getPair())setPair(3, 4)console.log(getPair())
Now, just as we did for makeState()
, let’s turn makePair()
into a generic function.
makePair
Here’s a generic version of makePair
.
F
and S
(for “F”irst and “S”econd).first
will be F
.second
will be S
.function makePair<F, S>() {let pair: { first: F; second: S }function getPair() {return pair}function setPair(x: F, y: S) {pair = {first: x,second: y}}return { getPair, setPair }}
Here’s an example usage. By calling makePair
with <number, string>
, it forces first
to be number
and second
to be string
.
// Creates a (number, string) pairconst { getPair, setPair } = makePair<number,string>()// Must pass (number, string)setPair(1, 'hello')
To summarize, you can create a generic function that takes multiple type parameters.
// makeState() has 1 type parameterfunction makeState<S>()// makePair() has 2 type parametersfunction makePair<F, S>()
Of course, you can also use the extends
keyword or default types like before:
function makePair<F extends number | string = number,S extends number | string = number>()
You can even make the second type (S
) to be related to the first type (F
). Here’s an example:
// The second parameter S must be either// boolean or whatever was specified for Ffunction makePair<F extends number | string,S extends boolean | F>()// These will workmakePair<number, boolean>()makePair<number, number>()makePair<string, boolean>()makePair<string, string>()// This will fail because the second// parameter must extend boolean | number,// but instead it’s stringmakePair<number, string>()
Let’s go back to our previous implementation of makePair()
. Now, take a look at the type of pair
:
function makePair<F, S>() {let pair: { first: F; second: S }// ...}
This works as is, but if we want to, we can refactor { first: F, second: S }
into an interface or a type alias so it can be reused.
Let’s first extract the type of pair
into a generic interface. I’ll use A
and B
as type parameter names to distinguish them from the type parameters of makePair()
.
// Extract into a generic interface// to make it reusableinterface Pair<A, B> {first: Asecond: B}
We can then use this interface to declare the type for pair
.
function makePair<F, S>() {// Usage: Pass F for A and S for Blet pair: Pair<F, S>// ...}
By extracting into a generic interface (an interface that takes type parameters), we can reuse it in other places if necessary.
Alternatively, we can extract it into a generic type alias. For object types, type aliases are basically identical to interfaces, so you can use whichever one you prefer.
// Extract into a generic type alias. It’s// basically identical to using an interfacetype Pair<A, B> = {first: Asecond: B}
To summarize, you can create generic interfaces and type aliases, just as you can create generic functions.
To learn more about interfaces v.s. type aliases, read this StackOverflow answer. As of TypeScript 3.7, which added a support for recursive type aliases, type aliases can cover pretty much all of the use cases of interfaces.
The last thing we’ll cover is generic classes. First, let’s revisit the code for makeState()
. This is the generic version that doesn’t use extends
or default type parameters.
makeState()
function makeState<S>() {let state: Sfunction getState() {return state}function setState(x: S) {state = x}return { getState, setState }}
We can turn makeState()
into a generic class called State
like below. It looks similar to makeState()
, right?
class State<S> {state: SgetState() {return this.state}setState(x: S) {this.state = x}}
To use this, you just need to pass a type parameter on initialization.
// Pass a type parameter on initializationconst numState = new State<number>()numState.setState(1)// Prints 1console.log(numState.getState())
To summarize, generic classes are just like generic functions. Generic functions take a type parameter when we call them, but generic classes take a type parameter when we instantiate them.
You need to set "strictPropertyInitialization": false
on TypeScript config (tsconfig.json
) to get the above code to compile.
Thanks for reading! That’s all you need to know about generics in TypeScript. Hope I made generics less scary to you.
If you’d like me to write about some other topics on TypeScript, or if you have feedback, please let me know on Twitter at @chibicode.
The source code for this site is on GitHub:
About the author: I’m Shu Uesugi, a software engineer. The most recent TypeScript project I worked on is an interactive computer science course called “Y Combinator for Non-programmers”.
You can learn more about me on my personal website. My email is shu@chibicode.com.