Generics allow us to parameterize types, which unlocks great opportunity to reuse types broadly across a TypeScript project.
This is a somewhat abstract concept, so let’s start by grounding ourselves in a practical example.
A motivating use case
In an earlier chapter, we discussed the concept of dictionary data structures that could be typed using index signatures:
tsTryconstphones : {[k : string]: {customerId : stringareaCode : stringnum : string}} = {}phones .home phones .work phones .fax phones .mobile 
Let’s take as a given that sometimes it is more convenient to organize collections as key-value dictionaries, and other times it is more convenient to use arrays or lists.
It would be nice to have some kind of utility that would allow us to convert a “list of things into” a “dictionary of things”.
So, let’s treat this array of objects as our starting point:
tsTryconstphoneList = [{customerId : "0001",areaCode : "321",num : "123-4566" },{customerId : "0002",areaCode : "174",num : "142-3626" },{customerId : "0003",areaCode : "192",num : "012-7190" },{customerId : "0005",areaCode : "402",num : "652-5782" },{customerId : "0004",areaCode : "301",num : "184-8501" },]
… and this as what we aim to get in the end…
tsTryconstphoneDict = {"0001": {customerId : "0001",areaCode : "321",num : "123-4566",},"0002": {customerId : "0002",areaCode : "174",num : "142-3626",},/*... and so on */}
In the end, we hope to arrive at a solution that will work for any list we wish to transform into an equivalent dictionary — not just this one specific use case.
We will need one thing first — a way to produce the “key” for each object we encounter in the phoneList array. To remain flexible, we will design our function such that whoever is asking for the list-to-dictionary conversion should also provide a function that we can use to obtain a “key” from each item in the list.
Maybe our function signature would look something like this:
tsTryinterfacePhoneInfo {customerId : stringareaCode : stringnum : string}functionlistToDict (list :PhoneInfo [], // take the list as an argumentidGen : (arg :PhoneInfo ) => string // a callback to get Ids): { [A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.2355A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.k : string]:PhoneInfo } {// return a dictionary}
Of course, we will see an error message as things stand right now, because we haven’t implemented the function yet.
This isn’t too difficult to implement. Let’s make a very specific solution right now with a forEach function - which we can refactor and generalize as a next step.
tsTryfunctionlistToDict (list :PhoneInfo [], // take the list as an argumentidGen : (arg :PhoneInfo ) => string // a callback to get Ids): { [k : string]:PhoneInfo } {// create an empty dictionaryconstdict : { [k : string]:PhoneInfo } = {}// Loop through the arraylist .forEach ((element ) => {constdictKey =idGen (element )dict [dictKey ] =element // store element under key})// return the dictionaryreturndict }constresult =listToDict (phoneList , (item ) =>item .customerId )console .log (result )
Click the Try button for the code snippet above, click “Run” in the TypeScript playground, and you should see that this solution works for our specific example.
Now, let’s attempt to generalize this, and make it so that it works for lists and dictionaries of our PhoneInfo type, but lots of other types as well. How about if we replace every PhoneInfo type with any…
tsTryfunctionlistToDict (list : any[],idGen : (arg : any) => string): { [k : string]: any } {// nothing changed in the code belowconstdict : { [k : string]: any } = {}list .forEach ((element ) => {constdictKey =idGen (element )dict [dictKey ] =element })returndict }constdict =listToDict ([{name : "Mike" }, {name : "Mark" }],(item ) =>item .name )console .log (dict )dict .Mike .I .should .not .be .able .to .do .this .NOOOOOOO 
Ok, this works at runtime if we test it in the TypeScript playground, but every item in our dictionary is an any. In becoming more flexible and seeking to handle a variety of different items, we essentially lose all of our helpful type information.
What we need here is some mechanism of defining a relationship between the type of the thing we’re passed, and the type of the thing we’ll return. This is what Generics are all about
Defining a type parameter
Type parameters can be thought of as “function arguments, but for types”.
Functions may return different values, depending on the arguments you pass them.
Generics may change their type, depending on the type parameters you use with them.
Our function signature is going to now include a type parameter T:
ts
Let’s look at what this code means.
The TypeParam, and usage to provide an argument type
- <T> to the right of listToDict
 means that the type of this function is now parameterized in terms of a type parameterT(which may change on a per-usage basis)
- 
list: T[]as a first argument
 means we accept a list ofT‘s.- TypeScript will infer what Tis, on a per-usage basis, depending on what kind of array we pass in. If we use astring[],Twill bestring, if we use anumber[],Twill benumber.
 
- TypeScript will infer what 
Try to convince yourself of these first two ideas with the following much simpler (and more pointless) example:
tsTryfunctionwrapInArray <T >(arg :T ): [T ] {return [arg ]}
Note how, in the three wrapInArray examples below, the <T> we see in the tooltip above is replaced by “the type of the thing we pass as an argument” - number, Date, and RegExp:
tsTrywrapInArray (3)wrapInArray (newDate ())wrapInArray (newRegExp ("/s/"))
Ok, back to the more meaningful example of our listToDict function:
tsTryfunctionlistToDict <T >(list :T [],idGen : (arg :T ) => string): { [k : string]:T } {constdict : { [k : string]:T } = {}returndict }
idGen: (arg: T) => string is a callback that also uses T as an argument. This means that…
- we will get the benefits of type*checking, within idGenfunction
- we will get some type-checking alignment between the array and the idGenfunction
tsTrylistToDict ([newDate ("10-01-2021"),newDate ("03-14-2021"),newDate ("06-03-2021"),newDate ("09-30-2021"),newDate ("02-17-2021"),newDate ("05-21-2021"),],(arg ) =>arg .toISOString ())
One last thing to examine: the return type. Based on the way we have defined this function, a T[] will be turned into a { [k: string]: T } for any T of our choosing.
Now, let’s put this all together with the original example we started with:
tsTryfunctionlistToDict <T >(list :T [],idGen : (arg :T ) => string): { [k : string]:T } {constdict : { [k : string]:T } = {}list .forEach ((element ) => {constdictKey =idGen (element )dict [dictKey ] =element })returndict }constdict1 =listToDict ([{name : "Mike" }, {name : "Mark" }],(item ) =>item .name )console .log (dict1 )dict1 .Mike constdict2 =listToDict (phoneList , (p ) =>p .customerId )dict2 .fax console .log (dict2 )
Let’s look at this closely and make sure that we understand what’s going on:
- Run this in the TypeScript playground, and verify that you see the logging you should see
- Take a close look at the types of the items in dict1anddict2above, to convince yourself that we get a different kind of dictionary out oflistToDict, depending on the type of the array we pass in
This is much better than our “dictionary of anys”, in that we lose no type information as a side effect of going through the list-to-dictionary transformation.
Best Practices
- Use each type parameter at least twice. Any less and you might be casting with the askeyword. Let’s take a look at this example:
tsTryfunctionreturnAs <T >(arg : any):T {returnarg // 🚨 an `any` that will _seem_ like a `T`}// 🚨 DANGER! 🚨constfirst =returnAs <number>(window )constsameAs =window as any as number
In this example, we have told TypeScript a lie by saying window is a number (but it is not…). Now, TypeScript will fail to catch errors that it is suppose to be catching