TypeScript Generic Types and How to Properly Implement Them

Generic Types are a powerful TypeScript tool often misused due to them being so different from what a developer coming from traditional JavaScript is used to

Pablo A. Del Valle H.
The Startup
6 min readJun 10, 2020

--

Learn TypeScript’s Generic Types and their usages
Photo by Iza Gawrych on Unsplash

In TypeScript, Generic Types enable us to define a placeholder type for a variable we do not know enough yet to precise its type. Consider the following snippet:

The previous function will work quite well if we are sure that the argument send, when sayMessage is called, will always be a string. However, there might be situations that could prevent that to happen. For example, the message argument is coming from a third party, say a service, database, input field, etc. Another, reason why this might cause issues, later on, is the possibility of the design of the application changes, and suddenly it’s supposed to accept numbers.

Let’s take on the last condition. Let’s assume we received a change request to allow the sayMessage function to, in addition to strings, receive also numbers. Maybe our first hunch is to do something like this:

And that would work. After all, that’s what Union Types are for. However, I’d argue that this is a poor architectural pattern and should be used sparingly and only on punctual cases; since this approach, as a result, leaves our simple function not suitable to scale. Say that a new request comes in, asking to allow null values to be accepted as parameters:

You might even be tempted to try this:

Well… at this point, I bet you start to feel a little bit uneasy on your initial hunch. There is a better solution.

Implementing Type Variables as Function Parameters

A more elegant way to solve the aforementioned problem would be to implement Type Variables (otherwise known as “Generics”). Consider the following snippet:

So, a number of things going here. First, you’ll notice the <T extends unknown> right before the parameter list of the function. T is in fact the representation of the Generic Type. T is what traditionally is used for these Generic Types (T stands for Type), however, you can use whatever you want for the Generic Type name. The “extends unknown” bit has to be added for lambda functions, otherwise, you’ll get a syntax error. For example, non-lambda functions can define the exact same signature outlined previously as follows:

Just a different way to express the same sentiment. I personally prefer lambda functions, but to each it’s own!

The next thing we could point out is that our message parameter is not a string or a number or a string | number anymore, but rather a T type. This allows us to do the following:

All previous examples are valid since sayMessage now accepts Generic Type arguments.

One cool thing about using Generic Types is that the type of the parameter can also be used within the body of the function and even be returned:

The previous code will return a tuple comprised of the composed string (Simon says “T variable”) and the actual <T> parameter passed as the second argument.

Constraining the Types in a Generic

Our previous example of a generic parameter might be too broad and will allow the consumer of the function to call it sending in types such as undefined, null, or even an object. Let’s prevent that by using Generic Constraints.

Let’s limit how much the user of the function can send in:

The special syntax of “T extends keyof M” is telling us that the parameter T is a member of the first parameter — type M — . So, essentially, we are constraining the possible options for this parameter to, in our example, 3 choices: ‘greeting’, ‘farewell’, or ‘love’.

Using an Interface to constraint a Generic

Another neat way to use a Generic Type is by implementing Interfaces which is also another way you can constraint the scope of your type. The following example is actually a 2 in 1 example:

First, we create an IMessage interface. This interface, in turn, implements a Generic Type (M). The interface is quite simple, establishing an object with two members: message (this is the generic type M) and an emotion, which is a string.

Then, our sayMessage function delineates that the parameter it receives (message) is a Generic Type that extends the IMessage interface. This ensures that this parameter will comply with the contract established there (requires to have a message and an emotion).

Notice the special notation for this Generic Type: <T extends IMessage<string>>, this ensures that the message member of the IMessage interface is a string.

TypeScript’s Utility Types

In TypeScript, you can actually see Generic Types being used in many places, as a matter of fact, there’s even have a specific section for them in their documentation.

For example, one of the preferred ways to define an Array in TypeScript makes use of Generics:

This definition, applying all the knowledge gained from this article, tells us that the variable containers will exercise the HTMLTableElement that was defined as a Generic Type within the Array’s interface for TypeScript, which has this signature:

Another built-in Generic Type you might find using a lot is the NonNullable one. This one gets defined as a custom type in TypeScript source code:

Quite an interesting definition. Here we see a ternary conditional where, if whatever comes from the <T> type is an extension of either null or undefined, it will return a never type (“never types” deserve a whole new chapter of their own) essentially making it “impossible” to be either or null or undefined, otherwise, it will return the passed T type.

I’d encourage you to explore TypeScript built-in Utility types as they might save you architecture design as well as development time.

Conclusion

Generics are supported by many well-established programming languages such as C# or Java, it only makes sense to implement them in TypeScript as well.

One of the best use cases for Generics in TypeScript is to provide a type-safe variable for unknowns, such as retrieving API data sets or accepting user input. However, this is not the only use case:

  • Provide better readability of your code
  • Help you catch unsuspected scenarios with unknown variables
  • Let your IDE give you better IntelliSense suggestions
  • Assist TypeScript to properly consume otherwise non-type-safe data (API responses, user input, calculations, parsing a JSON file, etc.)
  • Keep you from assigning the ‘any type’ to unknown variables
  • Properly type your variable will also serve a pseudo documentation purpose, whereas you read the code and understand what the variables are used for

Thanks for reading!

Find me on LinkedIn, Medium, GitHub.

--

--