Advanced TypeScriptAdvanced8 min06 / 7

Generics

Learn how to write flexible, reusable functions and containers that stay fully type-safe by using type parameters — the angle-bracket superpower of TypeScript.

You have probably written a function that works perfectly for one type of data and then thought, "I wish I could reuse this exact logic with a different type." The tempting fix is to reach for any — it works at runtime, but quietly throws away everything TypeScript promises you. Generics are the better answer. They let you write code once and reuse it with any type, while TypeScript still tracks exactly what type flows through at every step. In this lesson you will learn how type parameters work, why they beat any, how to use them on functions and interfaces, and how to add constraints so TypeScript can give you even more help.

Think of it like

Generics are like a labelling machine

Imagine a labelling machine at a warehouse. You feed it a box, it prints a label that says exactly what is inside, and that label travels with the box forever. any is a box marked "stuff" — nobody knows what's in it. A generic is the labelling machine: whatever type you put in, the correct label comes out the other side.

#The Problem: When any Is a Trap

any works at runtime but loses type information — TypeScript cannot help you after the function returns
function identity(value: any): any {
  return value;
}

const result = identity(42);
// result is typed as `any` — TypeScript has forgotten it is a number
// This would not catch a bug if value were a string:
console.log(result.toFixed(2));

#Type Parameters: The <T> Syntax

Declare T between angle brackets, use it as the parameter and return type. TypeScript infers the actual type from each call — no explicit type annotation needed on the caller's side. You can write identity<number>(42) but almost never have to.
function identity<T>(value: T): T {
  return value;
}

const num = identity(42);       // TypeScript infers T = number
const str = identity("hello");  // TypeScript infers T = string

console.log(num.toFixed(2));    // OK: num is number
console.log(str.toUpperCase()); // OK: str is string
// str.toFixed(2);              // Error: string has no toFixed

#Generic Interfaces and Array Helpers

Box<T> and first<T> each work for any type while remaining fully typed
// Generic interface
interface Box<T> {
  label: string;
  value: T;
}
const scoreBox: Box<number> = { label: "score", value: 99 };

// Generic array helper — returns the first element or undefined
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const top = first([98, 75, 60]); // type: number | undefined
console.log(top);                // 98
console.log(first([]));          // undefined

#Constraints with extends

Sometimes you do not want any type — you need one that has at least certain properties. The extends keyword adds a constraint to T. TypeScript then lets you safely access those properties inside the function, while still accepting any compatible type.

T extends HasLength accepts strings, arrays, or anything else that has a length property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(`Length: ${item.length}`);
  return item;
}

logLength("typescript"); // Length: 10
logLength([1, 2, 3]);    // Length: 3
// logLength(42);        // Error: number has no .length
Common mistake

extends in a constraint is not inheritance

In a class, extends means "inherits from". In a generic constraint, it means "must satisfy this shape". T extends HasLength means: T must have at least a length: number property — not that T must be a subclass of anything.

Quick check

What does TypeScript infer as the type of `result` in this call? ```ts function identity<T>(value: T): T { return value; } const result = identity(true); ```

Tip

Start without constraints, add them when TypeScript asks

Write your generic with just <T> first. If TypeScript reports "Property X does not exist on type T", that is the signal to add an extends constraint. This keeps your generics as flexible as possible and only narrows them when the code actually needs it.

Key takeaways

  • A type parameter `<T>` is a placeholder that TypeScript replaces with the real type at each call site — giving you reuse without losing safety.
  • Generics preserve type information through functions and containers; `any` throws it away.
  • TypeScript usually infers `T` from your arguments, so you rarely need to write the type argument explicitly.
  • Use `extends` to constrain `T` to types that have certain properties, unlocking access to those properties inside your function.
  • Generic interfaces like `Box<T>` let you build reusable data structures that remain fully typed for any content type.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

TypeScript infers T from the argument. What does this code print at runtime?

predict-output
function identity<T>(value: T): T {
  return value;
}

const str = identity("hello");
console.log(str.toUpperCase());
Fix the bug#2

This code has a bug — what's wrong?

fix-bug
function logLength<T>(item: T): T {
  console.log(`Length: ${item.length}`);
  return item;
}

logLength("typescript");
Fill in the blank#3

Complete the type parameter declaration so this identity function is generic over a type T, reusing the lesson's <T> syntax.

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

const num = identity(42); // T inferred as number
Reorder the lines#4

Put these lines in the correct order to define a generic Box<T> interface, then create a Box that holds a number — matching the lesson's example.

1
  label: string;
2
const scoreBox: Box<number> = { label: "score", value: 99 };
3
interface Box<T> {
4
}
5
  value: T;
Your turn
Practice exercise

Write a generic function called lastItem that accepts an array of any type and returns the last element, or undefined if the array is empty. Then write a generic interface Pair<T, U> that holds two values of potentially different types — first and second. Finally, create a Pair where first is a number and second is a string.

Try it live — edit the code and hit Run to see the output:

solution.ts · editable