Free Handbook · Runs in your browser

Generics

Write a function, interface or class once and let it work with any type, without giving up the safety and autocomplete a specific type gives you.

0 / 105 lessons🔥 0 day streak
ShareXLinkedIn

Module 06 · what you'll be able to do

  • Explain why a generic beats any for a reusable function
  • Write generic functions, interfaces and classes with type parameters
  • Constrain a type parameter to shapes that have what the function needs
  • Give a type parameter a default, and use more than one at once
  • Recognize the classic generic patterns: a typed Stack, a typed first-of-array helper
01

Generic functions

A function that only ever handles one type is easy to type: (x: number) => number. A function meant to work with any type, while still keeping track of exactly which one it was given, needs a type parameter — written <T> right after the function name. T is a placeholder the compiler fills in with a real type at each call site.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
42
hello
Your turn
Change identity to take two parameters of the same type T, identity2<T>(a: T, b: T): T, returning whichever one is truthy, and call it with two numbers.
VisualizeWhat identity(42) and identity("hello") actually do at runtimeStep 1 / 4
function identity<T>(x: T): T {
return x
}
const a = identity(42)
const b = identity("hello")
console.log(a, b)
Line 4

identity(42) is called. The compiler infers T = number just to type-check this one call — at runtime T does not exist at all; x is simply the value 42, an ordinary function argument.

Variables now
x42
All 4 steps as a table
StepLineWhat happenedVariables now
14identity(42) is called. The compiler infers T = number just to type-check this one call — at runtime T does not exist at all; x is simply the value 42, an ordinary function argument.x = 42
22Returns x unchanged.a = 42
35identity("hello") is a separate call. T is inferred as string this time, purely for the compiler — the function body that actually runs is identical bytecode either way.b = 'hello'
46Prints both results.
any is not the same thing as a generic
function identity(x: any): any also compiles and also accepts anything — but it throws the type away. Call it with a number and the return type is any, not number, so the compiler can no longer catch a mistake like calling .toUpperCase() on the result. A generic preserves the type through the function; any discards it.
Type parameter
A placeholder type, conventionally named T, U, K, V, declared in angle brackets and filled in by the compiler at each call.
Generic function
A function whose parameter or return types are expressed in terms of one or more type parameters instead of a fixed type.
JuniorWhy use a generic function instead of typing a parameter as any?

any turns type checking off entirely for that value — you lose autocomplete and every future safety check on it. A type parameter like T preserves the actual type through the function: pass in a number and you get a number back, with the compiler aware of that the whole way, instead of writing a separate overload for every type you might pass.

What they are really testing: Whether the candidate understands the safety a generic preserves, not just that both compile.

02

Generic interfaces and classes

Interfaces and classes take type parameters the same way functions do. class Box<T> means every instance of Box is parameterized by whatever T was when it was constructed — a Box<string> and a Box<number> are different, incompatible types to the compiler, even though they share one class definition.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
hi
bye
Your turn
Create a second box, const n = new Box<number>(10), call .set(20) on it, and log n.get().
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
true 1
Your turn
Call wrap with a plain string, wrap("done"), and log .data from the result — notice the compiler still knows it is a string.
  • A generic interface like ApiResponse<T> is a reusable envelope shape — the same response wrapper for a User, a Product, or a plain string payload.
  • A generic class like Box<T> is a reusable container — the logic (get/set) is written once, the type it holds varies per instance.
  • Both are checked the same way: every place T appears inside the body must be consistent with whatever T turns out to be at the call or construction site.
Error you will hit

TS2322: mismatching a generic instantiation

typescript
class Box<T> {
  constructor(private value: T) {}
  get(): T {
    return this.value
  }
}
const numberBox: Box<number> = new Box<string>("hi")
app.ts:6:7 - error TS2322: Type 'Box<string>' is not assignable to type 'Box<number>'.
  Types of property 'value' are incompatible.
    Type 'string' is not assignable to type 'number'.

6 const numberBox: Box<number> = new Box<string>("hi")
        ~~~~~~~~~
Why the compiler said that

Even though Box is erased at runtime, the compiler still tracks each instantiation of T separately — Box and Box are different, incompatible types, the same way string and number themselves are.

The fix

Match the annotated type to the type actually constructed.

typescript
const numberBox: Box<number> = new Box<number>(5)
Mid-levelWhy does TypeScript let you write class Box, but the emitted JavaScript is just class Box?

Generic type parameters exist only for the compiler's type-checking pass — TypeScript has no reified generics, unlike Java or C#. There is no runtime representation of T anywhere; by the time tsc emits JavaScript, every angle-bracket type parameter is erased, and the class behaves identically no matter what it was instantiated with.

What they are really testing: Grasp of type erasure, a very common point of confusion for developers coming from Java or C#.

03

Generic constraints

An unconstrained T could be anything at all — a number, a boolean, an object with no useful shape — so the compiler will not let you use any property on it. <T extends SomeShape> narrows T to only types that are assignable to SomeShape, which lets you safely use whatever SomeShape guarantees inside the function body, while T can still be any type that satisfies it.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
apple
[ 1, 2, 3 ]
Your turn
Call longest with two plain objects that both have a length field, e.g. { length: 3 } and { length: 7 }, and log the result’s length.
Error you will hit

TS2339: using a property the compiler cannot guarantee exists

typescript
function longest<T>(a: T, b: T): T {
  return a.length >= b.length ? a : b
}
app.ts:2:12 - error TS2339: Property 'length' does not exist on type 'T'.

2   return a.length >= b.length ? a : b
             ~~~~~~
Why the compiler said that

T here is completely unconstrained — it could be a number, a boolean, anything at all — so the compiler cannot assume every possible T has a .length property, and refuses to let you read one.

The fix

Add a constraint that guarantees the property exists.

typescript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b
}
Error you will hit

TS2345: an argument that fails the constraint

typescript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b
}
console.log(longest(5, 10))
app.ts:4:22 - error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

4 console.log(longest(5, 10))
                     ~
Why the compiler said that

The constraint extends { length: number } requires T to have a length property. A plain number has no such property, so 5 cannot satisfy T at this call site.

The fix

Pass values that actually have a .length, like strings or arrays.

typescript
console.log(longest("a", "bbb"))
Mid-levelWhat does do?

It restricts T to any type that structurally has a .length property that is a number — arrays, strings, or a custom object with that field. Inside the function body, code can now safely read .length without the compiler complaining that T might lack it, while T itself can still be any type at all as long as it satisfies that shape.

What they are really testing: Understanding constraints as a structural bound, not a specific concrete type.

Quick check

Which of these could NOT be passed to a function constrained as ?

04

Default type parameters

A type parameter can have a default, written <T = string>, used whenever the caller does not supply one and it cannot be inferred from the arguments. It works exactly like a default function parameter value, just at the type level.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
2
Your turn
Create a second container explicitly typed for numbers, new Container<number>(), add three numbers to it, and log its count.
Defaults do not limit what can be passed
Container<T = string> still accepts new Container<number>() just fine — the default only fills in when nothing else determines T. It is a convenience for the common case, not a constraint.
05

Common generic patterns: a typed Stack, firstOf

Two shapes show up constantly once you start writing generic utilities: a small generic data structure (like a stack), and a generic helper that plucks a value out of an array while preserving its type.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
3
2
2
Your turn
Create a Stack<string> instead, push three names onto it, and log the result of two .pop() calls.
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
10
undefined
Your turn
Write a matching lastOf<T>(arr: T[]): T | undefined and call it on the same array of numbers.
PatternSignatureWhen to reach for it
Generic containerclass Stack<T>A data structure whose behavior does not depend on what it holds.
Generic accessorfunction firstOf<T>(arr: T[]): T | undefinedA helper that returns a piece of whatever was passed in, with the type preserved.
Generic wrapperinterface ApiResponse<T>An envelope shape reused across many different payload types.
06

Multiple type parameters

A function or class can take more than one type parameter, each varying independently — conventionally named T, U or, for a key/value pair, K, V. This is how you express "these two things do not have to be the same type, but the compiler should still track each one precisely."

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
age 30
Your turn
Write swap<A, B>(pair: [A, B]): [B, A] that reverses the tuple, call it with the result above, and log both values.
Where this shows up in real code
A key-value store (Map<K, V> itself is generic this way), a request/response pair (request<TReq, TRes>(body: TReq): Promise<TRes>), or anything that transforms one type into a related but different one, like mapValues<T, U> below.
SeniorHow would you design a generic function that maps over a Record and returns a Record, preserving key types?

Something close to function mapValues<T, U>(obj: Record<string, T>, fn: (value: T) => U): Record<string, U> { ... }. Two independent type parameters let the input value type and the transformed output type vary separately, while the compiler still enforces that every value handed to fn really is a T, and every result assigned back into the new record really is a U.

What they are really testing: Whether the candidate can compose multiple type parameters into a realistic utility, not just recite the syntax for one.

Finish the TypeScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.