AniUI Academy

Generics Fundamentals

Write one function that works across many types without losing type safety, using type parameters, constraints with extends, and default type arguments.

11 min read

Some functions and types genuinely don't care what type they're working with — they just need to work with some type consistently. Generics are how you write that once, instead of duplicating it per type or falling back to any and losing type safety entirely.

The problem generics solve

Here's a function that returns whatever it was given:

function identity(value: any): any {
  return value;
}
 
const result = identity("hello");
result.toUpperCase(); // compiles — but so would result.push(1), or anything else

any "works" here, but it throws away the one thing this function actually guarantees: whatever type goes in comes back out, unchanged. A type parameter captures that relationship instead:

function identity<T>(value: T): T {
  return value;
}
 
const result = identity("hello"); // T inferred as string
result.toUpperCase();             // fine — result is string, not any
 
const num = identity(42);         // T inferred as number
num.toUpperCase();
// Property 'toUpperCase' does not exist on type 'number'.

<T> declares a type parameter — a placeholder type, filled in per call. TypeScript infers T from the argument you actually pass, the same way it infers a variable's type from its initializer. You rarely need to specify it explicitly (identity<string>("hello")), though you can when inference alone wouldn't have enough to go on.

A more realistic example

function first<T>(items: T[]): T | undefined {
  return items[0];
}
 
const firstNumber = first([1, 2, 3]);      // T = number, so number | undefined
const firstName = first(["Amara", "Jae"]); // T = string, so string | undefined

One function definition, correctly typed for every array it's called with — no duplication, and no any anywhere in sight.

Constraints: T extends

An unconstrained T could be anything, which means you can't assume it has any properties or methods at all inside the function body:

function logLength<T>(value: T) {
  return value.length;
  // Property 'length' does not exist on type 'T'.
}

A constraint restricts what T is allowed to be, using extends — not the class-inheritance keyword from earlier, but the same word reused for "T must be assignable to this type":

function logLength<T extends { length: number }>(value: T) {
  return value.length; // fine — every T here is guaranteed to have .length
}
 
logLength("hello");      // fine — strings have .length
logLength([1, 2, 3]);    // fine — arrays have .length
logLength(42);
// Argument of type 'number' is not assignable to
// parameter of type '{ length: number; }'.

This is the pattern to reach for instead of any whenever you need "some type, but it must have at least these properties." It's strictly more precise than any (which allows literally everything, safe or not) and more flexible than a single concrete type (which would only work for one shape).

Multiple type parameters

Generics aren't limited to one parameter — a function combining two independent values commonly needs two:

function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}
 
const result = pair("age", 30); // [string, number]

Default type parameters

A type parameter can have a default, used when it can't be inferred and the caller doesn't specify one explicitly:

interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}
 
const generic: ApiResponse = { status: 200, data: "anything" }; // T defaults to unknown
const typed: ApiResponse<{ name: string }> = { status: 200, data: { name: "Amara" } };

This mirrors default function parameters — it's about ergonomics for the common case, while still allowing a more specific type argument when one is known.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • A type parameter (<T>) captures a relationship between a function's input and output types, without giving up type safety the way any would.
  • TypeScript infers T from the arguments at each call site — you rarely need to specify it explicitly.
  • <T extends Shape> constrains what T is allowed to be, so the function body can safely assume properties the constraint guarantees.
  • A default type parameter (<T = Default>) is used when T can't be inferred and the caller doesn't supply one.

Check yourself

4 questions · pass 3/4 to unlock Generic Interfaces and Classes

up to 50
  1. 1.What problem does function identity<T>(value: T): T { return value; } solve that function identity(value: any): any {} doesn't?

  2. 2.What does <T extends { length: number }> restrict a generic type parameter to?

  3. 3.In function wrap<T = string>(value?: T), what does = string do?

  4. 4.Why is function first<T>(items: T[]): T { return items[0]; } better than a version typed (items: any[]): any?

4 left to answer