Video

Watch the full video on YouTube:

Generic Types

Introduction

In this tenth chapter of our TypeScript series, we introduce generic types. Generics are one of the most powerful features in TypeScript, allowing you to create reusable and flexible code that works with multiple data types without losing type safety.

A generic can be visualized as a box that collects something, but only knows it collects something, without knowing specifically what that something is until you start putting things in it.

Generic Types

Basic Definition

To create a generic type, we use angle brackets <T> where T is a representation of a type that hasn’t been defined yet:

export type Box<T> = {
  content: T;
};

T is the common convention, but you can name it anything. The important thing is that it represents a type that only exists within the context of this type.

Using a Generic Type

Once defined, you can specify the type when using it:

// Box with a single number
export const aBoxOfNumber: Box<number> = {
  content: 123,
};

// Box of numbers (array)
export const boxOfNumbers: Box<number[]> = {
  content: [1, 2, 3],
};

TypeScript infers the type T based on what you pass. If you try to pass something that doesn’t match, it throws an error.

Generics with Complex Types

Generics aren’t limited to primitive types. You can use them with classes and interfaces:

type Pet = {
  id: number;
  name: string;
  owner: string;
};

export const boxOfPets: Box<Pet[]> = {
  content: [
    { id: 1, name: "Milo", owner: "Angel" }
  ],
};

Generic Functions

Generics also work with functions. This is useful when you need a function to work with multiple types while maintaining type safety.

Example: Environment Variables

Imagine you have an object with environment variables and want to create a function that accesses its values:

const variables = {
  API_KEY: "some-string",
  PORT: 3000,
  DSN: "dsn-string",
  IS_DEV: true,
};

A first approach without generics:

export function getEnvVar(key: string) {
  return variables[key as keyof typeof variables];
}

This approach has problems: TypeScript doesn’t know exactly what type it’s returning.

Generic Function with Constraints

The solution is to use generics with extends to restrict what values the function can receive:

export function getEnvVar<T extends keyof typeof variables>(key: T) {
  return variables[key];
}

How Constraints Work

  1. T extends keyof typeof variables: The generic T must be one of the keys of the variables object
  2. When passing "API_KEY", TypeScript knows it returns string
  3. When passing "PORT", TypeScript knows it returns number
  4. When passing "IS_DEV", TypeScript knows it returns boolean
const apiKey = getEnvVar("API_KEY"); // type: string
const port = getEnvVar("PORT"); // type: number
const isDev = getEnvVar("IS_DEV"); // type: boolean

TypeScript automatically infers the return type based on the key you pass.

Generics in JavaScript Classes

If you’ve worked with JavaScript before, you’ve probably seen generics in action without knowing it:

// Array generic
const arrayOfPets: Array<Pet> = [];

// Map generic
const mapOfPets = new Map<string, Pet>();

// Set generic
const setOfPets = new Set<Pet>();

These JavaScript classes use generics internally to define the type of elements they contain.

Generics with Default Values

You can define a default type for a generic using the syntax T = Type:

export type Box<T = string> = {
  content: T;
};

// If you don't define the type, uses string by default
export const boxOfStrings: Box = {
  content: "hello",
};

// But you can override the type
export const boxOfNumbers: Box<number> = {
  content: 42,
};

Multiple Generics with Optionals

If a type has multiple generics and the first is optional, the second must also be optional:

export type Box<T = string, Meta = null> = {
  content: T;
  meta: Meta;
};

// Uses default values
export const boxWithDefaults: Box = {
  content: "hello",
  meta: null,
};

// Define both types
export const boxWithMeta: Box<string, string> = {
  content: "hello",
  meta: "metadata",
};

Best Practices

  1. Use T as the convention for generic names, but you can use more descriptive names
  2. Apply constraints with extends to limit what types a generic can receive
  3. Use generics with default values to make your API more flexible
  4. Generics with keyof allow you to create functions that operate on an object’s keys
  5. Prefer generics over any to maintain type safety

Conclusion

Generic types are a fundamental tool in TypeScript that allows you to create reusable and flexible code. From simple types like Box<T> to functions with complex constraints, generics are the foundation for building robust and maintainable architectures.

Repository

💻 Repository: https://github.com/radagva/ts-example

Next Steps

🔔 Subscribe to the channel to not miss the next chapters of the TypeScript course!

📺 YouTube Channel

TypeScript Guide EP09: Partial, Required and NonNullableTypeScript Course EP11: Generics in Practice