Using Generics in TypeScript
Write your first generic function 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/
) undersrc/
in the app generated by thecreate-react-app
and a new filegenerics.ts
. Instructions to make thecreate-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 propertyfoo
. - 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:
-
It takes a lot of time.
-
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"))
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!