Dataslope logoDataslope

Generics Basics

Write reusable code without losing type information

Imagine you want to write a function that returns whatever you pass to it — an identity function. You could write:

function identity(x: any): any {
  return x;
}

This works at runtime, but you've lost all type information. If you call identity(42), the return type is any, so the compiler won't catch mistakes like identity(42).toUpperCase(). You've traded type safety for reusability.

Generics solve this problem. They let you write code that works with any type while preserving precise type information. The identity function becomes:

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

Now identity(42) returns number, identity("hello") returns string, and the compiler tracks the exact type through the function. This is the power of generics: abstraction without information loss.

Why generics matter

Without generics, you face a painful trade-off:

  1. Concrete types: Write identityNumber(x: number): number and identityString(x: string): string. This is type-safe but not reusable — you need a separate function for every type.
  2. any escape hatch: Write identity(x: any): any. This is reusable but not type-safe — the compiler can't help you anymore.

Generics give you the best of both worlds: one function, full type safety, and the compiler infers the type parameter based on what you pass in.

Code Block
TypeScript 5.7

The <T> is a type parameter (also called a type variable). It's a placeholder for "whatever type you pass in." When you call identity(42), TypeScript infers T = number and the return type becomes number. When you call identity("hello"), it infers T = string and the return type becomes string.

Type parameter naming

By convention, single-letter names like T, U, V are used for generic type parameters. If the parameter has a more specific role, use a descriptive name like TKey or TValue. Avoid overly generic names like Type or GenericType.

Generic functions

Let's look at more realistic examples. Suppose you want a function that wraps a value in an array:

Code Block
TypeScript 5.7

The type parameter T flows through the signature: the input is T, the output is T[]. The compiler infers T from the argument you pass, so you get precise types at every call site.

Multiple type parameters

You can have more than one type parameter:

Code Block
TypeScript 5.7

Here, A and B are independent type parameters. The compiler infers both from the arguments you pass. This is useful for functions that relate two different types (like a key-value pair, a tuple, or a mapping function).

Explicit type arguments

Usually, TypeScript infers type parameters from the arguments. But you can also provide them explicitly:

Code Block
TypeScript 5.7

When a generic function has no parameters (or the parameters don't help inference), you must supply the type argument explicitly in angle brackets: createArray<number>().

Generic interfaces and type aliases

Generics aren't limited to functions. You can parameterize interfaces and type aliases too.

Code Block
TypeScript 5.7

The Box<T> interface is parameterized by T. When you create a Box<number>, T is number. When you create a Box<string>, T is string. The same interface works for any type.

The Result<T, E> type alias is even more interesting: it's a discriminated union parameterized by two types — the success value type (T) and the error type (E). This is a common pattern for modeling operations that can succeed or fail.

Reusable abstractions

Generic interfaces and type aliases let you define reusable abstractions — containers, results, events, state machines — that work with any payload type. You write the logic once and reuse it everywhere.

Generic classes

Classes can also be generic. Here's a simple generic stack:

Code Block
TypeScript 5.7

The Stack<T> class holds items of type T. When you instantiate new Stack<number>(), all the methods operate on numbers. When you instantiate new Stack<string>(), all the methods operate on strings. One class, many types.

A practical example: map

Let's build a generic map function (like Array.prototype.map). It takes an array of type T[] and a function (T) => U, and returns an array of type U[].

Code Block
TypeScript 5.7

Notice how the compiler infers both T and U:

  • From numbers (a number[]), it infers T = number.
  • From the callback n => n * 2 (which returns a number), it infers U = number.
  • So the return type is number[].

For the second call, it infers T = number and U = string (from the callback returning a string), so the return type is string[].

This is the power of generics: you write one function, and the compiler figures out the specific types for every call site.

Type parameter flow

Understanding how type parameters flow through a function is key to using generics effectively. Let's trace through an example:

Code Block
TypeScript 5.7

The type parameter T is inferred from the input array. If you pass number[], T = number, so the return type is number | undefined. The | undefined is there because the array might be empty.

Challenge: Generic container

Now it's your turn. You'll implement a generic Container class that holds a value and provides methods to transform it.

Challenge
TypeScript 5.7
Generic Container

Implement a generic Container<T> class with:

  • A constructor that takes an initial value of type T.
  • A get() method that returns the current value.
  • A map<U>(fn: (value: T) => U) method that applies fn to the value and returns a new Container<U> (do not mutate the original).

The tests will verify that map transforms the value and returns a new container with the correct type.

Multiple choice check

QuestionSelect one

Given this generic function:

function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}

What is the return type of getFirst([1, 2, 3])?

T

number | undefined

number

When to use generics

Generics are the right choice when:

  1. You're writing a utility that works with any type. Functions like identity, wrap, first, last, map, filter, etc.
  2. You're defining a container or collection. Box<T>, Array<T>, Set<T>, Map<K, V>, etc.
  3. You're modeling results, events, or state machines with variable payloads. Result<T, E>, Event<TPayload>, State<TData>, etc.
  4. You want to preserve type information through a transformation. A function that takes T[] and returns U[] (like map), or a class that takes T and exposes T (like Promise<T>).

Don't use generics when:

  • The type is genuinely any or unknown and you don't care about precision.
  • You're only ever going to use it with one concrete type. (Though making it generic anyway can be fine for future-proofing.)

Start concrete, refactor to generic

If you're unsure whether you need generics, start with a concrete type (e.g., Box that holds number). If you later need it for other types, refactor to Box<T>. Don't prematurely genericize — but don't be afraid to add type parameters when the need arises.

Recap

Generics let you write reusable code that works with any type while preserving full type information. Instead of choosing between concrete types (verbose, not reusable) and any (reusable, not type-safe), you use type parameters to abstract over types.

Key takeaways:

  • Type parameters (like <T>) are placeholders for "whatever type you pass in."
  • Inference: The compiler usually infers type parameters from the arguments you pass. You rarely need to write them explicitly.
  • Generic functions: function identity<T>(x: T): T { return x; } — the parameter and return type are both T.
  • Generic interfaces and type aliases: interface Box<T> { value: T } — parameterize data structures.
  • Generic classes: class Stack<T> { /*...*/ } — one class, many types.
  • Multiple type parameters: function pair<A, B>(a: A, b: B): [A, B] — relate different types.

Generics are everywhere in TypeScript: Array<T>, Promise<T>, Map<K, V>, Set<T>, and countless libraries. Once you understand how they work, you'll start seeing opportunities to write more reusable, type-safe code everywhere.

Next: Generic Constraints, where we go beyond unconstrained type parameters and explore how to restrict T to types that have certain properties — unlocking even more powerful abstractions like keyof, extends, and conditional types.

On this page