Arya Bhimani

Are generics a good enough reason to use TypeScript?

Discussing how generics work in TypeScript

JavaScript is a dynamically typed language, and that is fantastic. But, it does add some confusion when you work with many developers together as a team. Interfaces, types, and generics can be a helping hand in developing reusable, type-safe, and bug-free code.

TypeScript Generics can really help elevate your code readability and usability. If you've seen TypeScript code that becomes so complicated that you see "any" type everywhere, it can usually be solved with a generic.

A generic is sort of a term to identify the signature of a method, which supports many different types. The most common generic which all of you have probably used is: Array . For example, you can make your datatype an array of any: Array<any> or an array of numbers: Array<number> , you get the point. Note that this is equivalent to number[] .

So the "generic" portion of this is where the datatype goes. Array is defined as Array<T> where "T" (could be any character between the < > ) is defined by the consumer of the generic type, you.

How Can I Incorporate This In My Code?

Let's say there's a method you create and you could pass in any of these types: a string, an array of numbers or strings, an array of a custom object like a User. Now, how wold you type this? You might go with something like:

function myFunction(arg: string | number[] | string[] | User[]);

That looks really messy. At some point, your method signature will get so large, that you will just end up doing the following, and destroying any benefits you would receive from using TypeScript.

function myFunction(arg: any[]);

Can we use a generic here? Absolutely! Let's give our method some meaning. The purpose of this method will be to return a random element from the list which is passed in. So instead of any array, we know that this is going to be a generic, and it will return one element from the array. We can define this as follows:

function getRandomElement<T>(list: Array<T>): T;

And we can use this method for any of the types mentioned above. It is important to note that types can either be defined explicitly by the developer, or they can be inferred in TypeScript. In the second example, el is inferred to be a string type, since we passed in an array of strings.

const n: number = getRandomElement<number>([4, 23, 42, 54]);
const el = getRandomElement(['hello', 'world', 'medium']);

Default Values for Generics

Usually, the default value of generics ends up being "any" if none is provided. But, you can provide a default value if you like:

function getRandomElement<T = number>(list: T[]): T;

Extending for Generics

What if, instead of just returning a random element, you were to return a User's age in your function? You would want to protect yourself against the age being undefined , right? This is where your generic type needs to extend something. Let's see this in practice, what if you have a User, Admin, and Person types:

interface User {
  name: string;
  age: number;
  data: WebData;
}
interface Person {
  name: string;
  age: number;
  data: PersonalData;
}
interface Admin {
  name: string;
  organization: string;
}
function getRandomAge(arr: User[] | Person[]): number;

We can see, that only User and Person have "age" property in them, and Admin does not. Instead of specifically typing your function to be "User" or "Person" in the example above, we can use a generic instead:

function getRandomAge<T extends { age: number }>(arr: T[]): number;

We know that the function will still return a number, but we are guaranteeing that the object we pass into getRandomAge has at least an age property, which is of type number. If we pass in an "Admin" into the function, TypeScript will not allow it:

Type 'Admin' is missing the following properties from type '{ age: number; }[]

Can We Still Improve?

In the previous section, we created a Person and User, both of these had the same three properties: name, age, and data. The only difference is that Person has a data of type "PersonalData" and User has a data of type "WebData". Can we reorganize this somehow?

Let's try and solve this using Generics. We know that all 3 property names are the same, so let's make an interface, with a Generic type.

interface GenericPerson<T> {
  name: string;
  age: number;
  data: T;
}

What this means is that, GenericPerson must be created with another type which is reserved for the "data" property of the object. Now, we can create our Person and User types using this GenericPerson. We will define a "type" instead of an interface.

type Person = GenericPerson<PersonalData>;
type User = GenericPerson<WebData>;

The above Person and User types will work the same way as our interfaces did in the previous section. The only difference is that we reduced the duplication of our code, and abstracted out some of the logic in our Generic interface.

In Summary

You can clearly see the benefits of using Generics in this case, because it allows your code to be type-safe and reusable. Using "any" can create potential bugs which are introduced and can only be detected at runtime. TypeScript allows you to catch these bugs before it reaches any running environment, because they are caught before compiling the code to JavaScript. If you'd like to learn more, you can follow along with my "Build An App" series which uses TypeScript in practice.

Go back home.

github.com/aryascripts