Node JSProgrammingTypescript

Generics: What Are They and How to Use Them in Typescript

Understanding TypeScript generics, what they are, how and when to use them

Introduction

If this isn’t the first time you’ve looked at learning what generics are then you’ve probably noticed that Google searches are predominantly filled with examples in C# and Java.

This isn’t by accident or coincidence as generics were added to Java and C# in 2004 and 2005 respectively, and being some of the most widely used languages, that is to be expected. It goes back earlier than that however. The first widely adopted use of generics come from ML, and HM previous to that.

There is often confusion when people refer to Lisp having or using generics. Strictly typed languages are the only place where such things belong.

Defining a Generic

Generics are a type of polymorphism (poly- being Greek for many and morph being Greek for shape). Literally translated to ‘having many shapes’. Often we give the description of ‘parametric polymorphism‘.

In simpler terms, it’s a way of specifying a type when you call the function, not when you write the function. To think of it another way, when writing a function who’s parameters may not be bound to one type; you can say to yourself “I’ll specify this later“.

Much in the same way that function and methods are passed parameters in brackets, generics are passed in less than and greater than characters.

type Lunchbox<T> = {...}

This simply means when we want to consume that Lunchbox type, we can pass in a type that gets referred to as T.

interface ISandwich {
    // ...
}
const myLunchbox: Lunchbox<ISandwich> = {
    // ...
}

Practical Example

// Queries the database and returns the result / results
public async query(query, params): any {
    const result = await DB.Query(query, params);
    return result;
}

The above function is a utility method for querying a database. So that you don’t need to call the same async await everywhere.

The problem with this, is you’ll always get an untyped array of rows from your database.

To fix this, we can use a generic type and pass it into the function as a type parameter.

public async query<T>(query: string, params: [string | number]): T[] {
    const result:T = await DB.Query(query, params);
    return result;
}
public async someThing() {
    type Product = {
        name: string,
        price: 12.55,
        stock: 7
        available: true
    };
    const products = await query<Product>(`SELECT * FROM stock WHERE price < ?;`, [20]);
}

So we’ve added a generic type T to our function. We have also typed our function parameters to string and an array union (string or number). And finally we have said that we are returning an array of type T.

Next we create a Product type in some other function. This describes the data that is in the product table schema.

Now when we call the query function, it will return us an array of type T.

typescript-generics-product-typed
TypeScript Generic Example

So now we have a single utility function that serves more than one purpose. Now, we don’t need to write multiple functions that essentially do the same thing. That is tedious, time consuming and really goes against DRY Principles.

More Complex Examples

Lets look at using generics in interfaces. Suppose we have a restaurant application and some code that’s used for adding new staff memebers.

type Chef = {
    position: string;
    cookingLevel: number;
};
type Waiter = {
    maximumTables: number;
}

We can make a reusable staff interface.

interface IStaff<T> {
    name: string;
    job: T;
}

As you can see, we’re passing in the generic type T into the interface. Then assigning property job to type T.

Now when we instantiate a new object of type IStaff we must specify the type of staff member they are.

// instantiating a new chef staff member
const newCook: IStaff<Chef> = {
    name: 'steve',
    job: {
        cookingLevel: 1,
        position: 'pot wash'
    }
}
// instantiating a new waiter member
const newWaiter: IStaff<Waiter> = {
    name: 'dan',
    job: {
        maximumTables: 30
    }
}

If we pass a cooking level, to a new Waiter type instance then the compiler generates us an error.

generics typescript error
TypeScript Generic Error 2322

Mixing Records & Generics

Following on from my previous article where we look at Dictionaries and Record Types, I’ll show you an example of how to mix the two.

Lets assume we are building a forum system where we need to keep in memory a list of moderators and editors. We want to make sure that no usernames can be duplicated so for simplicity we’ll use a dictionary type.

Lets construct our Moderator, Editor types as well as our Users Dictionary.

type Moderator = {
    canBan: boolean;
    canDelete: boolean;
    canApprovePosts: boolean;
}
type Editor = {
    postCount: number;
    forumScore: number;
}
type Users<T> = Record<string, { account: T }>

Now when we consume the Users type to create a dictionary object, we can pass in the type of user as a generic. All key pair values will now be strictly typed against T.

// Creating a list of editors
const editors: Users<Editor> = {
    'spongeBob': {
        account: {
            postCount: 1,
            forumScore: 0
        }
    },
    'patric': {
        account: {
            postCount: 6,
            forumScore: 15
        }
    },
}
// Creating a list of moderators

That just about covers a quick introduction into what Generics are, and how to use them. Stay tuned for follow up articles with more in-depth examples

Related Articles

Leave a Reply

Back to top button