This page looks best with JavaScript enabled

In-Depth Look at TypeScript Generics: Part 1 — Intro to Generics

 ·  ☕ 5 min read  ·  ✍️ Iskander Samatov

In Depth Look at TypeScript Generics Part 1 Intro to Generics


In this two-part series, we will take an in-depth look at TypeScript generics. This first post will give you a basic understanding of generics and how it works. In the later post, we’ll cover advanced inference and conditional types.

So, without further ado, let’s get started!

Why do we need generics?

Using generics allows us to write a typesafe code that will work with a wide range of primitives and objects.

Without generics, we would have to create a new type for every possible combination of data that we want to work with. With generics, we can write our function or method once and then reuse it for different types of inputs.

That sounds great, but what does TypeScript generics look like in practice? Let’s take a look at the syntax.

Generic syntax

TypeScript uses angled bracket <> and a type symbol T to indicate generic syntax. In your client code, TypeScript will replace the type T with the type you pass. This will make more sense if we take a look at an example:

1
2
3
4
5
6
function identifyType <T>(target: T) {
  console.log("Type of target is", typeof target);
}

identifyType("LOL") // "Type of target is", "string"
identifyType({word: "LOL"}) // Type of target is", "object"

The function in the code above takes a parameter of generic type T. It then prints the parameter’s type to the console using the typeof operator.

In the first call, we pass in a string, and in the second one, we pass in an object. Since it’s generic, this function will work with any data and will execute successfully in both instances.

Classes and interfaces can also use generics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Identifier<T> {
  seed: T;
  constructor(public newSeed: T) {
    this.seed = newSeed;
  }

  identifyType<T>(target: T) {
    console.log("Type of target is", typeof target);
  }
}

Here we defined a generic class with a constructor and the identifyType method. The type of argument that the identifyType method accepts must match the newSeed argument supplied to the constructor.

In other words, if you pass a string to the constructor, identifyType would only accept strings as arguments.

Side note: You don’t have to use T to indicate generics. T is just a convention commonly used in TypeScript.

By default, TypeScript tries to infer types based on the arguments, but you can use explicit typecasting to force a specific type using the bracket syntax <>:

1
identifyType<string>(1); // Argument of type 'number' is not assignable to parameter of type 'string'

In this example, the function identifyType expects an argument of type string but receives a number. Because of this mismatch, TypeScript will produce an error.

Constraining types

Usually, we want to limit the type of T to restrict the types our generic code accepts. We can do that using the extends keyword.

Say we want to limit the identifyType function to accept only strings and numbers. Here’s how we would do it:

1
2
3
4
5
  function identifyType<T extends string | number>(target: T) {
    console.log("Type of target is", typeof target);
  }

  identifyType(true) // Argument of type 'boolean' is not assignable to parameter of type 'string | number'

Using the extends keyword, we can tell TypeScript which types our function or class should accept. In our case, identifyType accepts only strings or numbers. When we attempt to pass a boolean, the function produces an error.

Using the type T

Generic code can only reference functions or properties of the objects that are common to any type of T. In other words, you can’t access anything specific to a particular type. You can only access methods and properties present in all of the generic types we specify.

For example, if we have a generic type that’s constrained to two types, we can only use functions and properties present in both of them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type birdGenerator = {
  generate: () => any[]
  birds: any[]
}

type ponyGenerator = {
  generate: () => any[]
  ponies: any[]
}

function generateAnimal<T extends birdGenerator | ponyGenerator> (generator: T) {
  generator.generate()
  generator.birds // ERROR: Property 'birds' does not exist on type 'ponyGenerator'.
}

In the example above, we would get an error if try to access the birds property within our generateAnimal function . However, we can access the generate method without a problem since it’s present in both birdGenerator and ponyGenerator types.

Generic constraints

You can construct a generic type out of another generic type. One way to do that is using the keyof keyword.

The keyof keyword is used to generate a new type based on the keys of another one.

We can use it to make sure we can only specify keys that are present in our generic object, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function getPropertyValue<T, B extends keyof T> (target: T, key: B) {
  return target[key];
}

const myObj = {
  id: "0",
  name: "John Doe",
  age: 19
}

getPropertyValue(myObj, "id")
getPropertyValue(myObj, SSN) // ERROR: Cannot find name 'SSN'.

Here we use the keyof keyword to create a new type and restrict the possible values for the second argument of the getPropertyValue function. Now it only accepts strings that match the keys present in the myObj object.

Conclusion

In this article, we covered the basics of TypeScript generics. We looked at using generics to constrain the types our code accepts and explored some of the features available when working with generic objects.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.