Lesson 6 of 13 · 0 of 1 exercises solved
Generics
let one or work with many types while staying type-safe. You will learn to read and write type like `<T>`.
// Generic function — works with any type T
function identity<T>(value: T): T {
return value
}
const n = identity(42) // T inferred as number
const s = identity("hello") // T inferred as string
const explicit = identity<boolean>(true)
// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: "Alice", age: 30, active: true }
const name = getProperty(user, "name") // TypeScript knows this is string
const age = getProperty(user, "age") // TypeScript knows this is number
// getProperty(user, "email") // Error: "email" not in user
// Generic class
class Stack<T> {
private items: T[] = []
push(item: T): void { this.items.push(item) }
pop(): T | undefined { return this.items.pop() }
peek(): T | undefined { return this.items.at(-1) }
get size(): number { return this.items.length }
isEmpty(): boolean { return this.items.length === 0 }
}
const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
console.log(numStack.pop()) // 2
const strStack = new Stack<string>()
strStack.push("hello")
// strStack.push(42) // Error: number not assignable to string// Multiple type parameters
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
return arr1.map((item, i) => [item, arr2[i]])
}
const pairs = zip([1, 2, 3], ["a", "b", "c"])
// pairs is [number, string][]
// Generic constraints
interface HasLength {
length: number
}
function longest<T extends HasLength>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
console.log(longest("hello", "hi")) // "hello"
console.log(longest([1, 2, 3], [1, 2])) // [1, 2, 3]
// console.log(longest(1, 2)) // Error: number has no length
// Generic interfaces
interface Repository<T extends { id: number }> {
findById(id: number): T | undefined
findAll(): T[]
save(item: T): void
delete(id: number): void
}
// Generic type aliases
type Nullable<T> = T | null
type Optional<T> = T | undefined
type Result<T, E extends Error = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
function divide(a: number, b: number): Result<number> {
if (b === 0) return { ok: false, error: new Error("Division by zero") }
return { ok: true, value: a / b }
}
const res = divide(10, 2)
if (res.ok) console.log(res.value) // 5Common question
Do I need generics when any exists?
A generic is a function, class or type that takes types as parameters, so one definition works for many types without losing precision. <T> is a stack of whatever the caller picks, and the compiler tracks that choice through every push and pop. Constraints (T extends Something) let the generic rely on what T can do while staying open.
Reach for a type parameter whenever a value goes in and a related value comes out. Give parameters meaningful names when there are several, constrain them when you need a property, and let inference pick T at the call site so callers rarely write the angle brackets themselves.
Source: <T>, <T> and Map<K, V> are all generic. If you use them, you already rely on generics every day.
Common mix-ups
Each of these sounds reasonable. Think about why it is wrong, then open the explanation.
A function that takes any handles every type, which is all generics do.
A generic type parameter is a runtime value the function can inspect.
Generics are for authors and too abstract for app code.
Practice
Write a generic function firstOr<T>(items: T[], fallback: T): T that returns the first element or the fallback, and a generic class Stack<T> with push, pop (returns T | ) and a size getter.
Generic function and class
function firstOr(items, fallback) {
return fallback
}
class Stack {
items = []
}
console.log(firstOr([3, 4], 0), firstOr([], "none"))
const s = new Stack()
s.push("a"); s.push("b")
console.log(s.pop(), s.size)
Press Run to try your code, then Check answer to grade it.Tip: use the ← and → keys to move between lessons.