Using Generics in TypeScript

When writing code for front-end UIs in React, we often hear about the importance of creating components that are reusable. We can do this by keeping components small or by composing them in an organized fashion. For example, we might pull out variables that can be modified and make them props so that the component can be reused for any use case.

We should try to achieve the same outcome for noncomponent code in our codebase - our functions and classes that we build alongside or outside of our React components. In large applications, it’s very often the case that many functionalities are frequently reused all across the app, on multiple pages, areas, and most importantly, in a variety of different data types. In this lesson, we’ll investigate how generics help us reuse valuable functionalities like searching, sorting, and filtering in applications.

The first example

This example is a simplified version of generic sorting.

Note: To follow along with the rest of this lesson, make a new folder (generics/) under src/ in the app generated by the create-react-app and a new file generics.ts. Instructions to make the create-react-app can be found in External Environment Setup, which is located in the appendix. For the rest of this lesson, we should write code in a TypeScript file with an editor that has IntelliSense. There will be a full, live app example towards the end of the lesson that we can use to check our work.

Let’s start by defining a simple interface IFooBar:

interface IFooBar {
foo: string;
bar: string;
}

This interface has only two properties, foo and bar, both of type string.

Let’s associate the interface with some real data that have this type. Let’s define a const fooBars, which will be an Array of IFooBar. We’ll fill it with easily understandable data so it’s easy to check if our sort function is working properly later in the lesson:

const fooBars: Array<IFooBar> = [
{
foo: "Foo C",
bar: "Y Bar"
},
{
foo: "Foo B",
bar: "Z Bar"
},
{
foo: "Foo A",
bar: "X Bar"
}
]

Let’s imagine that somewhere in our app, we want to sort data that has the type IFooBar. In this situation, we could receive an array of IFooBar from an API endpoint. To sort the data, we could write a sortByFoo function using the following process:

  • First, accept our fooBars array.
  • Next, use the sort function, the built-in JavaScript array, to explicitly sort with the property foo.
  • Finally, return the sorted array.
function sortByFoo(fooBars: Array<IFooBar>) {
fooBars.sort((a, b) => {
if (a.foo > b.foo) {
return 1;
}
if (a.foo < b.foo) {
return -1;
}
return 0;
})
}

The same logic would follow if we wanted to sort by the other property, bar, creating a function sortByBar():

function sortByBar(fooBars: Array<IFooBar>) {
return fooBars.sort((a, b) => {
if (a.bar > b.bar) {
return 1;
}
if (a.bar < b.bar) {
return -1;
}
return 0;
})
}

These solutions work great for data that only has the properties foo and bar, but it’s easy to imagine situations with more complex types of data that have dozens of different properties. Writing explicit sort functions for all our properties is problematic for two reasons:

  1. It takes a lot of time.

  2. It introduces repetitive code that does nearly the same task (sorting).

Enter generics

This situation is a perfect use case for generics in TypeScript. We can create a generic function, called sortByKey, that will replace both sortByFoo and sortByBar and will also be easily extendable later. For example, we could add an additional property called hello to IFooBar.

interface IFooBar {
foo: string;
bar: string;
hello: string; // New property! Oh no! We didn't write a sort function for this one!
}

Let’s learn how to write this generic function! We need to use angle bracket syntax (< and >) to signal to TypeScript that we are using generics. A common pattern for generics is to start with the capital letter T for the generic type that needs to be provided. So, to start our sorting function, we’ll add a <T> after the function name.

function sortByKey<T>() {
}

When developers need more than one generic type, the most common pattern is to continue with the next letters of the alphabet, such as the capital letters U and V, separated by commas. For example, if we needed three generic types for sortByKey, the function signature could be sortByKey<T, U, V>. We can see a real-world example of generics in the TypeScript types for the class components in React in the code line class React.Component<P = {}, S = {}, SS = any>. In the example below, the React team has opted for a more literal convention of their generic type identifiers. They still use capital letters, but instead of using T, U, or V, they use letters related to what each generic variable represents—in this case P for props, S for state, and SS for snapshot.

interface IAppProps {
}
interface IAppState {
}
interface IAppSnapShot {
}
export default class App<IAppProps, IAppState, IAppSnapShot> {
}

This example is a little more advanced because it’s rare for three different generic types to be seen or required. Often a single generic type is more than enough to get the reusability needed for many functionalities.

Now, we need to add parameters to our function. In sortByFoo and sortByBar, we explicitly provided a parameter Array<IFooBar>. We want to use a generic type T as the parameter type instead. In other words, our function should be able to handle an array of any type T. In TypeScript notation, this looks like Array<T>. This array can be of any type since it’s a generic type, so it makes sense to give it a generic name. Let’s name it data and add it as the first parameter of our sortByKey function.

function sortByKey<T>(data: Array<T>) {
}

The keyof operator

We still need to add the ability to pass a key name to sort with. Again, we can rely on the power of TypeScript, using its keyof type operator. The keyof type takes a literal union of the types of keys. In this case, the type we’ll use is our generic type T. TypeScript is smart enough that we can use the keyof type operator even for generic types. Let’s finish writing the signature of our function sortByKey.

function sortByKey<T>(data: Array<T>, key: keyof T) {
}

Now let’s write the body of the function. The body of sortByKey won’t be too different from that of sortByFoo or sortByBar, except that we need to trade out the explicitly used keys of bar or foo for our generic key variable. Since we’ve used keyof T, TypeScript won’t object when we access a or b with key, in this case the a[key] or b[key], because key is quite literally keyof T.

function sortByKey<T>(data: Array<T>, key: keyof T) {
return data.sort((a, b) => {
if (a[key] > b[key]) {
return 1;
}
if (a[key] < b[key]) {
return -1;
}
return 0;
})
}

That’s it! We can now generically sort any data type anywhere in our app!

Twofold benefits

By using generics, not only is our function reusable across our entire app, but it prevents runtime errors when we try to sort data.

For example, these two lines of code below will both work. TypeScript won’t produce an error because foo and bar are keys of the IFooBar interface.

// Both fine: foo and bar are properties of IFooBar!
sortByKey<IFooBar>(fooBars, "foo")
sortByKey<IFooBar>(fooBars, "bar")

But, if we try to sort fooBars by the cat property, TypeScript will immediately underline cat in red.

// TypeScript complains: cat is not a property of IFooBar!
sortByKey<IFooBar>(fooBars, "cat")

Hovering over the error will show a warning. Click “Run” in the interactive example below to see the error, and comment out that final line of code to see our sortByKey function in action.

interface IFooBar {
foo: string;
bar: string;
}
const fooBars: Array<IFooBar> = [
{
foo: "Foo C",
bar: "Y Bar"
},
{
foo: "Foo B",
bar: "Z Bar"
},
{
foo: "Foo A",
bar: "X Bar"
}
]
function sortByFoo(fooBars: Array<IFooBar>) {
fooBars.sort((a, b) => {
if (a.foo > b.foo) {
return 1;
}
if (a.foo < b.foo) {
return -1;
}
return 0;
})
}
function sortByBar(fooBars: Array<IFooBar>) {
fooBars.sort((a, b) => {
if (a.bar > b.bar) {
return 1;
}
if (a.bar < b.bar) {
return -1;
}
return 0;
})
}
function sortByKey<T>(data: Array<T>, key: keyof T): Array<T> {
return data.sort((a, b) => {
if (a[key] > b[key]) {
return 1;
}
if (a[key] < b[key]) {
return -1;
}
return 0;
})
}
// Let's test our functiona and log our results out to the console.
// Both calls here are fine: foo and bar are properties of IFooBar!
console.log("Sort by 'foo':")
console.log(sortByKey<IFooBar>(fooBars, "foo"))
console.log("Sort by 'bar':")
console.log(sortByKey<IFooBar>(fooBars, "bar"))
// TypeScript complains: cat is not a property of IFooBar!
// (Comment out this line to see the results above properly logged.)
console.log(sortByKey<IFooBar>(fooBars, "cat"))
Full example including sortByFoo and sortByKey

We wouldn’t see this warning if we were using Vanilla JavaScript. We might only find this at runtime, and it might cause some confusion when trying to figure out why sorting by the cat property doesn’t actually sort the list at all!

Generics are awesome!

Generics are pretty powerful, right? It gets even better. This is only the beginning when it comes to the power of using generics with TypeScript!