April 19, 20234 min read

TypeScript Generics

Generics don't exist in JavaScript but are one of the essential concepts in TypeScript. They offer the best of both worlds: flexibility and type safety.

Last Updated April 19, 2023
Joker on top of scattered cards
Photo by Archana GS

TypeScript Generics

Let’s continue our TypeScript series. Today, we will look at a concept that’s pretty confusing for me - generics. If you’re coming from other languages like C# or Java, you’re probably familiar with them. But there is a high chance you’re like me - focused mainly on JavaScript (and maybe you’re confused too). There is no such concept in the vanilla language and it may cause problems. But it’s an essential concept of TypeScript. So, I’ll explain generics (to you and myself) like we’re eight years old.

What is a Generic?

Usually, when we work with types, we define them like this.

Defining Types in TypeScriptTS
1let person: string
2person = "Sam"
3
4let surname = "Fisher"

You define a variable, initialize it (or not), define a type, or let TypeScript infers it. And it’s done. The type won’t change. That’s why we use TypeScript, right? Yes, but sometimes we want to be more flexible. However, we want to avoid returning to the dynamic types of JavaScript. To achieve balance and type-safety, here comes - generics. A generic is like a variable but for types. A variable can receive different values - a generic can accept different types. A generic is a type connected with some other one, and it’s flexible regarding which that other type is. What do I mean by that?

Built-in Generics

If you look closely, some basic types like Array or Promise are generics in TypeScript. In vanilla JavaScript, arrays can store multiple different types. We want to be more precise using TS. For example, defining an array of strings can give TS more information and give us better auto-completion. We can write it two ways. We can use the standard syntax…

Basic Types and GenericsTS
1let hobbies: string[] // The standard syntax.
2let hobbies: Array<string> // The other syntax using generics.
3hobbies = ["sneaking"]
4
5// TypeScript infers it's a string and provides auto-completion.
6hobbies[0].split("")

…or we can use generics. It’s an array of strings. It’s a type connected to another one. A promise is also a generic type. It works with other types because it eventually returns other ones.

Generic Function

An alternative syntax for basic types is ok, but now let’s write some custom generics. Let’s see these variables for types in action.

Generic FunctionTS
1function merge<T, U>(a: T, b: U) {
2 return {
3 ...a,
4 ...b,
5 }
6}
7
8merge<string, string>("Sam", "Fisher")
9merge<string[], string[]>(["Sam"], ["Fisher"])
10merge({ name: "Sam" }, { hobbies: ["sneaking"] })

To define generics, we need additional pair of (pointing) brackets. The convention is to define generics using single letters starting from the “T” letter. The “a” argument is of type “T,” and the “b” is of type “U.” These types are not static. They are dynamic - set when we call the function. TypeScript can still infer the return value and provide auto-completion. Generics allow you to work with your data in TS optimal way. Look at the above snippet - we can pass objects, arrays, or strings. Generic functions allow you to fill concrete types for different function calls. “So, I can pass other types like numbers too?” Yes, and that is problematic because we use the spread operator.

Generic Constraints

Generic constraints come to the rescue. Sometimes we want flexibility, but at the same time, we want some validation. You can set certain limitations for the type generic is based on. You can use the extends keyword to define generic constraints.

Generic ConstraintsTS
1function merge<T extends object, U extends object>(a: T, b: U) {
2 return {
3 ...a,
4 ...b,
5 }
6}
7
8// TypeScript error:
9// Argument of type 'string' is not assignable to parameter of type 'object'.
10merge("Sam", "Fisher")

Now our function only accepts objects, and it avoids runtime errors. But let’s go further with our example. Let’s say that later in the code, we want to be sure that there is a specific property. We can define interfaces and tell our parameters to extend them.

Generic InterfacesTS
1interface BasicInfo {
2 name: string
3}
4
5interface AdditionalInfo {
6 hobbies: string[]
7}
8
9function merge<T extends BasicInfo, U extends AdditionalInfo>(a: T, b: U) {
10 return {
11 ...a,
12 ...b,
13 }
14}
15
16const person = merge({ name: "Sam" }, { hobbies: ["sneaking"] })
17person.name // You can be sure there is the name property.

Let’s imagine another scenario - we want to access an object property by passing a string. In such a scenario, we can use the keyof keyword. The first parameter in the below example should be an object. The second parameter should be any key in that object. The keyof operator takes an object type and produces a string or numeric literal union of its keys.

The keyof keywordTS
1function extract<T extends object, U extends keyof T>(obj: T, key: U) {
2 return obj[key]
3}
4
5extract({ name: "Sam" }, "name")
6// TypeScript error:
7// Argument of type '"hobbies"' is not assignable to parameter of type '"name"'.
8extract({ name: "Sam" }, "hobbies")

To read more about literal or union types, check my post about basic TypeScript types.

Generic Class

Similarly to functions, you can write generic classes. The syntax is the same - using angled brackets. Such a class can be both flexible and strongly typed.

Generic ClassTS
1class Data<T extends string | number | boolean> {
2 private data: T[] = []
3
4 addItem(item: T) {
5 this.data.push(item)
6 }
7
8 removeItem(item: T) {
9 this.data.splice(this.data.indexOf(item), 1)
10 }
11}
12
13const text = new Data<string>()
14text.addItem("Sam")
15text.addItem(50) // TypeScript error
16
17const number = new Data<number>()
18number.addItem(50)

We didn’t lock the above class to one type. At first, TypeScript doesn’t know which type it is. But then TypeScript knows the concrete type you pass in - when you call the function or instantiate the class. You can even be more granular and introduce new generic types in class methods or properties. That’s a good use case when you don’t need them in the entire class.

Generics inside classesTS
1class Data {
2 constructor(private data: Array<string | number | boolean> = []) {}
3
4 getItems() {
5 return [...this.data]
6 }
7}
8
9const string = new Data(["Sam", "Fisher"])
10const number = new Data([68])
11const object = new Data([{ name: "Sam" }]) // TypeScript error.

Generic vs. Union Types

Seeing the previous snippet, you may ask, “how that’s different from the union type?” Yeah, they look similar, but they work differently. I think the best way to explain the difference will be to use examples. Let’s go back to our class.

Generic vs. Union TypesTS
1class Data<T extends string | number | boolean> {
2 private data: T[] = []
3
4 addItem(item: T) {
5 this.data.push(item)
6 }
7
8 removeItem(item: T) {
9 this.data.splice(this.data.indexOf(item), 1)
10 }
11}

Writing our class like this using generics, we say: “You have to choose the type of data you want to store, and then you’re only allowed to add that type of data.” Use generics if you want flexibility but then want to stick to a specific type. It’s a different story using union types.

Generic vs. Union TypesTS
1class Data {
2 private data: (string | number | boolean)[] = []
3
4 addItem(item: string | number | boolean) {
5 this.data.push(item)
6 }
7
8 removeItem(item: string | number | boolean) {
9 this.data.splice(this.data.indexOf(item), 1)
10 }
11}

Here we’re not saying this is either an array of strings, or array of numbers, or an array of booleans. The array can store strings, numbers, and booleans mixed. It’s the same for functions. With union types, we accept any of these types every time we call the method.

Generic Utility Types

There are also many examples of generic utility types. They are like a set of helper functions that assist you with common type transformations. These types are available globally and were added gradually to new TypeScript versions. I list a couple of them.

Partial

In vanilla JavaScript, you can create an object and then add properties. TypeScript doesn’t like on-fly adding. But let’s say you need such a functionality.

Partial Utility TypeTS
1interface Computer {
2 name: string
3 description: string
4}
5
6function updateComputer(computer: Computer, fieldsToUpdate: Partial<Computer>) {
7 return {
8 ...computer,
9 ...fieldsToUpdate,
10 }
11}
12
13const desktop: Computer = {
14 name: "MacBook Pro",
15 description: "It has a dedicated GPU.",
16}
17
18const laptop = updateComputer(desktop, { description: 'It has a 16" display.' })

Partial wraps a type and changes its properties to optional. In our example, we must specify the name and description when creating an object. But, using partial, we don’t need to pass both values when updating one of the objects. Fields are optional, and we can update them independently.

Required

It’s the opposite of partial. It constructs a type consisting of all properties of the passed type set to required. With that knowledge, we can rewrite our previous snippet.

Required Utility TypeTS
1interface Computer {
2 name?: string
3 description?: string
4}
5
6function updateComputer(
7 computer: Required<Computer>,
8 fieldsToUpdate: Computer
9) {
10 return {
11 ...computer,
12 ...fieldsToUpdate,
13 }
14}
15
16const desktop: Required<Computer> = {
17 name: "MacBook Pro",
18 description: "It has a dedicated GPU.",
19}
20
21const laptop = updateComputer(desktop, { description: 'It has a 16" display.' })

Readonly

It works similarly to the other two. The difference is it changes all properties to read-only. You can’t reassign them later.

Readonly Utility TypeTS
1interface Computer {
2 id: string
3}
4
5const laptop: Readonly<Computer> = {
6 id: "ga6sd798",
7}
8
9// Cannot assign to 'id' because it is a read-only property.
10laptop.id = "ags98kd"

It wouldn’t make much sense to list all utility types and copy the TypeScript documentation, so I’ll end here. Grammarly (spell checking tool I use) says that “Your text is likely to be understood by a reader who has at least an 8th-grade education (age 13-14) and should be fairly easy for most adults to read.” That’s over eight years old, but I hope the post was clear anyway. Generic types are there to make your life easier and give you a combination of flexibility and type safety. I hope this post wasn’t too generic, and now you can use generics to your advantage.

Profile picture where I'm in a white t-shirt on gray gradient background. Half of my face is in the shadow. The other half is correctly illuminated.

Software engineer with polymath aspirations.

Read more. Stay curious

Half of a record on white background
September 1, 2022 • 4 min read

Accessible animations in React

Or how not to spin your users round (like a record). Some animations can make users sick. We'll take care of them and make non-essential animations optional.

Read post
List of CSS variables in Visual Studio Code.
September 14, 2022 • 4 min read

Converting design tokens to CSS variables with Node.js

Converting design tokens is an error-prone process - I found about it the hard way. So, I made a simple Node.js script that will help me with that task.

Read post
Five metal gears on a black brackground
September 23, 2022 • 6 min read

Gatsby with Netlify CMS

In this post, we will look closely at a Netlify CMS. It is an example of a new type of CMS that is git-based. We will integrate it with a Gatsby example project.

Read post

A newsletter that sparks curiosity💡

Subscribe to my newsletter and get a monthly dose of:

  • Front-end, web development, and design news, examples, inspiration
  • Science theories and skepticism
  • My favorite resources, ideas, tools, and other interesting links
I’m not a Nigerian prince to offer you opportunities. I don’t send spam. Unsubscribe anytime.