Code Your Path Coding School

Best TypeScript Generics Tutorial 2024: Use Generics Today

TypeScript Generics

Table of Contents

My absolute fav interpretation of TypeScript Generics is as fill-in-the-blank types. Generics help organize your code and create reusable components.

Welcome to the Best TypeScript Generics Tutorial 2024!!  Generics are a cornerstone of TypeScript,  flexibile to create highly adaptable functions, classes, interfaces, and more!

By the end of this tutorial, you’ll understand how generics work, when to use them, and how they can make your code more robust and maintainable.

What Are TypeScript Generics And When Should You Use Them?

Think of generics as “arguments (type parameters) to TS Types,” similar to how you provide arguments to JS functions. This makes generics the same way useful for creating reusable code, just as functions are useful when they accept arguments.

For example, without arguments in functions, I need two separate functions if I want to print two different strings:

function printStringFoo() { console.log('foo') }
function printStringBar() { console.log('bar') }

However, with argument support, I can write a single function that can print ANY string:

function printString(stringToPrint) { console.log(stringToPrint) }

The same pattern works for type generics — define the actual type when you use the generic and reuse the same definition for different types.

function printString<T>(str: T) { console.log(str) }

Here, <T> is my generic type parameter, which allows me to specify that stringToPrint can be of any type.

T is a generic type parameter that stands for “thing” or “type” — use it to name reusable type placeholders.

When you see <T>, it means “of things” or “of a thing.” For example, Array<T> means an array of things, where I can replace T with a specific type: string – Array<string>, or number – Array<numbers>.

Generic Chilli Shelf – Example For 5-Year-Old Kids

Suppose you make chili sauce (chili sauce = data) in jars (jars = variables), using habanero, jalapeño, and cayenne peppers.

You want to organize these chili sauces onto the shelves (shelf = container) that your wife labeled (shelf label = container type) accordingly: “Habanero Chilie,” “Jalapeño Chilie,” and “Cayenne Chilie.” Your wife — a TypeScript type checker — ensures you place each sauce on the correct shelf.

In this case, you will sort the jars and keep all the shelves! Otherwise, your wife will ask you to rearrange the jars to match the shelf’s label.

Chilie Jars Shelf - TypeScript Generics Shelf

Add a “Chilie Jars” shelf and place all the sauces on this new generic shelf! Replace three identical shelves with just one, and make your wife happy — fewer shelves to check!!

Basics Of Generic Type Variable In TypeScript

Try viewing TypeScript’s static type checking and JavaScript’s dynamic runtime as separate “worlds.”

In JavaScript, you’re passing arguments to functions. In TypeScript, you pass type parameters to generic types.

TS types exist primarily at compile time – catching errors and ensuring type safety net! While JS runtime is where your code actually executes.

This separation offers advanced features like TS generics without impacting the performance or behavior of your JS code at runtime. And you can provide better type safety and predictability of your code!!

Generics provide you access to a type that may vary throughout your program and to reference that type elsewhere. When one type in a function depends on another, it usually indicates that you need a generic function.

Write Reusable Components With Generic Type Parameters

Imagine you’re writing a function expecting either a number or a string. You could type this function as number | string, which would work fine…

…However, this is a case where you want the flexibility to accept ANY type. Use TS generics so that users of your function can pass their own types.

function identify<T>(value: T): T {
    return value;
}

let output = identity<number>(42); // output is of type number

let output2 = identity<string>('Hello!'); // output is of type string

identify is a generic function that takes a single argument of type T and returns the same type T. TypeScript will use T as a placeholder — the function can work with ANY type now!

Users can specify the actual type when they call the function –– identity<number>(42) and identity<string>(‘Hello!’).

I love this flexibility for creating reusable and type-safe JS code.

Generics And Computer Science

Regarding computer science, TypeScript generics — “type constructor” — is an implementation of Parametric Polymorphism.

Some mathematicians call functions “constructors” or “value constructors” because they create (construct) new output values out of input values.

Parametric polymorphism lets you write functions and classes that operate various types. This means I can use the same code with different types without modification. 

In general, polymorphism — a code that works with different types. TypeScript achieves this through generics, where a user supplies the type parameter. 

A type constructor creates a new type from another type. However, in programming, type constructors operate at the type level — not the value level!

TypeScript may (or may not) fully implement Parametric Polymorphism. If you want to delve deeper into this concept — start with this wiki article!

TypeScript Generics

Implement Generics in TypeScript Interfaces

Generics was a challenging concept to grasp fo me. They involve conceptual understanding and syntax familiarity, which you want to practice separately and, eventually, together.

When I first encountered generics, it was tough to wrap my head around. The new syntax, new brackets, and code were a black box at first…

…However, having “aha” moments signed that I was learning and making progress!!

Keep pressing on and continue to explore this concept. Compare how much you know about generics today to how much you knew 6 or 10 weeks ago — you’ll see significant improvement!!

To master generics, you may need a few more months and exposure to actual problems that only generics can solve.

I broke down the concept into pieces and related them to real-world scenarios. The goal — explain generics more intuitively! And make TS accessible and less intimidating for beginners (as it was, and sometimes still is, for me)

Basic Generics Naming Rules

When you see Array<T>, the T represents a placeholder that can be filled with any type. The generic Array<T> allows me to create arrays of different types depending on the context.

Generic type parameter uses the letter T in many programming languages. In TypeScript, use T when the generic type is truly generic and doesn’t have a specific semantic meaning.

function reverseArray<T>(array: T[]): T[] {
    return array.slice().reverse();
}

reverseArray function uses the T generic to indicate it can work with any type! However, cases like these are not few.

Use the descriptive names for generics whenever possible as it makes your code more readable and easier to understand.

type Item<TProductName extends string> = {
    productName: TProductName;
    // other properties...
};

TProductName is a generic type that extends string — it is clear what the generic type represents!

Common practice — add a T prefix to the type name:

type KeyValuePair<TKey, TValue> = {
    key: TKey;
    value: TValue;
};

One advantage of meaningful naming is the improved code editor (IntelliSense) experience. VSCode will show more informative and contextually relevant hints.

For example, consider generic type names — T versus TUser:

// Less descriptive
function getUser<T>(id: number): T {}

// More descriptive
function getUser<User>(id: number): User {}

In the second case, the VSCode provides more specific User type hints — easier to understand the expected type and properties.

Default Type To TS Generic

Sometimes, you want to set default types for generic arguments to provide a fallback type.

  1. Provide a default,
  2. Omit the argument when using the type (function, class, or interface),

and TS will use the default type instead!

function createArray<T = number>(length: number, value: T): T[] {
    return Array(length).fill(value);
}

In the example, the generic T has a default number type. When I call the createArray function without specifying a type argument, the TS compiler uses a number as the default!

// Using the default type (number)
let numericArray = createArray(3, 10); // inferred as number[]

// Specifying a different type
let stringArray = createArray<string>(3, "hello"); // explicit as string[]

Remember that you can only omit generic type arguments if all have defaults. Also, if one argument has a default, all following (subsequent) ones must too!

Generics are specific relationships in which we can vary the “inner” types, but the “outer” type must be CONCRETE!

Generics With Types In TypeScript

Suppose you’re writing a definition of a sale agreement.

You can write “An agreement to sell a car in exchange for money,” but what if someone wants to sell a laptop or stocks? Instead, write: “An agreement to sell a THING in exchange for money.” Then, you can sell cars, laptops, or stocks.

TypeScript follows the same concept with a generic SaleAgreement<T> type.

But what if you want to sell something in exchange for something other than money, like crypto?

Update your definition to read, “An agreement to sell thing A in exchange for thing B.” The TS generic type will look like SaleAgreement<TProduct, TCurrency>:

// Define the SaleAgreement generic type
type SaleAgreement<TProduct, TCurrency> = {
    product: TProduct;
    price: number;
    currency: TCurrency;
};

// Example usage
const carSale: SaleAgreement<string, string> = {
    product: "Car",
    price: 20000,
    currency: "USD"
};

const laptopSale: SaleAgreement<string, string> = {
    product: "Laptop",
    price: 1500,
    currency: "EUR"
};

console.log(carSale);
console.log(laptopSale);

TProduct and TCurrency are generics that I replace with specific types (string) for SaleAgreement instances.

The carSale and laptopSale variables demonstrate using the SaleAgreement type to define different agreements.

Use Generics With Interfaces Safely

Using generics in TypeScript provides stronger type checks at compile time. TS conceptualizes types at build time, while JS uses values at runtime.

During type checking, no actual values exist because values can only materialize when the program runs. Similarly, types are irrelevant during JS runtime and get removed during compilation.

We mainly use generics to let types act as parameters. Because I can access the type as a parameter, it helps to reuse the same code for various input types.

Without generics, you need separate interfaces for different types. For example:

interface ComediesSearchResults {
    popular: Comedy[];
    recent: Comedy[];
}

interface HorrorSearchResults {
    popular: Horror[];
    recent: Horror[];
}

However, with generics, you can create a single generic interface that can work with different types:

interface SearchResults<Type> {
    popular: Type[];
    recent: Type[];
}

SearchResults<Type> is a generic interface that can hold arrays of any type. I will specify the actual type when I use the interface – SearchResults<Comedy> or SearchResults<Horror>.

// Define your Comedy and Horror types
interface Comedy {
    title: string;
    rating: number;
}

interface Horror {
    title: string;
    scareFactor: number;
}

// Use the SearchResults generic interface with different types
const comedies: SearchResults<Comedy> = {
    popular: [
        { title: "Superbad", rating: 8.1 },
        { title: "The Hangover", rating: 7.7 }
    ],
    recent: [
        { title: "Game Night", rating: 7.0 },
        { title: "Blockers", rating: 6.2 }
    ]
};

const horrors: SearchResults<Horror> = {
    popular: [
        { title: "The Conjuring", scareFactor: 9 },
        { title: "Get Out", scareFactor: 8.6 }
    ],
    recent: [
        { title: "A Quiet Place", scareFactor: 8.1 },
        { title: "Hereditary", scareFactor: 7.3 }
    ]
};

console.log(comedies);
console.log(horrors);

The TS compiler ensures that the popular and recent properties are arrays and that array methods are available but not other methods.

With TS generic, I can reuse the same SearchResults interface across other video collections, making the code more flexible!

You probably do not need the generic unless it models relations between parameters (or for the return type).

Generic Object Type In React

In React apps with TypeScript, we use generics when defining components all the time!

interface Props {
  // Define your props here
}

const Component: React.Component<Props> = (props) => {
  // Component logic here
  return (
    // JSX markup
  );
};

React.Component defines a functional component with Props as its props interface. The Component expects an object as its props, which should conform to the Props interface.

TypeScript infers the correct types for the props, helping us avoid using incorrect or missing properties (thank you, TypeScript!).

Pass Type Parameter To Generics Class

You can define more generalized algorithms with TS generics. Imagine you have an Account class that you use as a base class. This Account contains methods and properties standard to all types of accounts.

class Account { 
    constructor(public balance: number) {}

    // ...other fields and methods
}

Later, you write several other classes — Checking, Saving, and Credit — that extend the base Account class.

class Checking extends Account { constructor(balance: number) { super(balance); } }

class Saving extends Account { constructor(balance: number) { super(balance); } }

class Credit extends Account { constructor(balance: number) { super(balance); } }

Now, imagine you want to add a getBalance function to each class. Without TS generics, you would need to define separate functions for each class:

function getCheckingBalance(account: Checking): number { return account.balance; }

function getSavingBalance(account: Saving): number { return account.balance; }

function getMoneyCreditBalance(account: Credit): number { return account.balance; }

This works but requires you to write and maintain multiple similar functions.

With generics, you can create a function that performs operations on any of these extended classes:

function getBalance<TAccount extends Account>(account: TAccount): number {
    // Implementation logic here

    return account.balance;
}

The <TAccount extends Account> syntax tells that the getBalance function accepts any subclass of Account. TypeScript ensures the function works correctly with Checking, Saving, or Credit accounts instances.

This way, you write a single function ( instead of three similar ) and make your code reusable!!

Using Generics With Functions

A generic type parameter in a function is useful when you want to create reusable code that can work with different types.

Every function call involves two lists of arguments – value and type arguments. Value arguments are listed in parentheses (a, b), while type arguments are listed in angle brackets <AType, BType>

When you call a function in Typescript, the type argument list can be implicit.

function identity<T>(arg: T): T {
    return arg;
}

// Type argument 'number' is inferred based on the argument '10'.
let result = identity(10);
console.log(result); // Output: 10

// Type argument 'string' is inferred based on the argument '"Hello"'.
result = identity("Hello");
console.log(result); // Output: Hello

The T type argument is optional, and the TS type checker will attempt to infer type arguments when I don’t specify them on line 6 (identity(10)) and 10 (identity(“Hello”)).

Similarly, when you declare a function, you can omit the type parameter list if the function doesn’t take any type parameters.

The key takeaway is that there are always TWO lists of arguments, even if you don’t explicitly see both. This shifted my TS generic perspective to me!!

Create mapped types with generics

In a mapped type, you iterate over the keys of an existing type and apply a transformation to the associated properties. For example, you can change the property types, make them optional or read-only, or even rename them!

The MyEventHandlers type is an example usage of the EventHandlers mapped type.

I created the Event type that maps Event fields with corresponding handlers. 

// Define the Event and EventHandlers types
type Event = {
    click: { x: number; y: number };
    hover: { element: HTMLElement };
    keypress: { key: string };
};

type EventHandlers<TEvent> = {
    [THandler in keyof TEvent]: (event: TEvent[THandler]) => void;
};

type MyEventHandlers = EventHandlers<Event>;

// Implementing the MyEventHandlers type
const myEventHandlers: MyEventHandlers = {
    click: (event) => {
        console.log(`Clicked at coordinates (${event.x}, ${event.y})`);
    },
    hover: (event) => {
        console.log(`Hovering over element:`, event.element);
    },
    keypress: (event) => {
        console.log(`Key pressed: ${event.key}`);
    }
};

// Usage examples
myEventHandlers.click({ x: 100, y: 200 }); // Logs: "Clicked at coordinates (100, 200)"
myEventHandlers.hover({ element: document.body }); // Logs: "Hovering over element: [object HTMLBodyElement]"
myEventHandlers.keypress({ key: 'Enter' }); // Logs: "Key pressed: Enter"

I define the Event, EventHandlers, and MyEventHandlers types and a myEventHandlers object.

When we apply EventHandlers to the TEvent type, we get a new type –– MyEventHandlers –– where the click, hover, and keypress properties expect functions!

The myEventHandlers defines functions for handling click, hover, and keypress events. I call these functions with appropriate (to each field) event objects.

Semantic Names in Mapped Generics

Semantic names shine in the types mapping. The UserAgeMap type uses the Key generic to create a new type: [Key in keyof User].

It maps only the age property of the User type to number, while mapping all other properties to never.

type User = {
    id: number;
    name: string;
    age: number;
};

type UserProperties = keyof User; // 'id' | 'name' | 'age'

// Using semantic names in mapped types
type UserAgeMap = { [Key in UserProperties]: Key extends 'age' ? number : never };

Semantic names like Key make it clear that I am iterating over the keys of the User type.

Creating Conditional Types With Generics

type IsString<T> = T extends string ? "Yes" : "No";

isString is a conditional type that takes a generic type parameter T.

The condition T extends string checks whether T is a subtype of string. If it is, the type resolves to “Yes” –– otherwise, it resolves to “No”!

type Test1 = IsString<string>; // "Yes" type Test2 = IsString<number>; // "No"

TS assigns the type “Yes” to Test1 because the string extends the string, lol. Test2 is assigned the type “No” ––  the number does not extend the string!

Use Case For Generics Interface – API Scenario

I love generic interfaces for storing additional data from API calls, like messages, errors, or metadata. Usually, I write a API response as a structure with data, success, error flags, and message field:

{
    data: {}, // Generic data
    success: boolean, // true/false
    error: boolean, // true/false
    message: string // Additional logging/errors/warnings
    // Other properties that apply to all objectstype
}

In TypeScript, you can define a generic ApiResponse interface to build this structure, where TData is the type of data coming from the API:

export interface ApiResponse<TData> {
    data: TData;
    success: boolean;
    error: boolean;
    message: string;
    // Other properties that apply to all objects
}

This approach allows me (and you, if you follow it, lol!) to handle different API responses in a type-safe manner!!

Generic Constraints Concepts

Assume a scenario where a function logs items from an array. Let’s define a Logable type with a log method (that returns nothing – void):

interface Logable {
    log: () => void;
}

To use it, I create a logProducts generic function that takes an array of items and inherits (extends) the Logable type:

function logProducts<TProducts extends Logable>(products: TProduct[]): void {
    products.forEach(product => product.log());
}

TProducts extends Logable is a TS type constraint. It ensures that the generic TProducts type is a Logable subtype — it must have all the properties and methods defined in the Logable interface (including the log method)!

With this function, it’s perfectly fine to pass an array of objects of any shape as long as the constraint is fulfilled:

logProducts([
    { log: () => console.log() },
    { newField: "Text Value", log: () => {} }
]);

In this case, TS allows extra properties (newField). The logProducts function doesn’t complain about the additional newField property in the second object.

However, TypeScrips works differently when we don’t use a generic type!

Create a new logProducts2 function, replace TProduct angle brackets <TProducts extends Logable>, and let the TS compiler infer type arguments.

function logProducts2(products: Logable[]): void {}

If I pass an array of objects with extra properties directly to logItems2, a compiler will throw an error!! sigh…

logProducts2([
    { log: () => {} },
    { newField: "Text Value", log: () => {} } // Error: Object literal may only specify known properties, and 'newField' does not exist in type 'Logable'
]);

However, you can still pass the array as an arr variable to the logProducts2 function without any errors:

const arr = [
    { log: () => {} },
    { a: "123", log: () => {} }
];

logItems2(arr);

Try this code on the playground

Use Of Generics Constraints – Example 2

Look at TypeScript types as sets of possible values. For instance, the { password: string } type represents the set of all possible objects that have a password property of type string. These objects might have other properties, but the username must be a string.

A TCustomArray extends unknown[] constraint means that TCustomArray can be any type but must fit within the larger set of unknown[], which includes all arrays and tuples.

type CustomArray<TCustomArray extends unknown[]> = TCustomArray;

// Example usage:
let myArray: CustomArray<[number, string]> = [1, 'a'];
let anotherArray: CustomArray<number[]> = [1, 2, 3];

I declared myArray variable with the type CustomArray<[number, string]>, which means it’s a tuple of a number and a string. The anotherArray variable is declared with CustomArray<number[]> type, meaning an array of numbers.

TS constraints allow flexibility in the TCustomArray data type as you assign it to an unknown[].

TypeScript Generics

Conclusion: Getting Started With TypeScript World Of Generics

Generics is not just a theoretical concept but a practical (and fantastic) tool!

I hope this guide gave you a solid foundation in TypeScript generics! Explore and apply JS and TS concepts in your projects –– Happy coding!

Share the Post:
Join Our Newsletter