Lesson 11 of 13
Conditional, Mapped & Template Literal Types
Learn conditional types, mapped types, infer and types. These are the tools used to build utility types like Partial and ReturnType, and they help you read type definitions.
// Conditional types — "if" for types
type IsArray<T> = T extends unknown[] ? true : false
type A = IsArray<string[]> // true
type B = IsArray<number> // false
// infer pattern-matches inside a type and pulls a piece out
type ElementOf<T> = T extends (infer U)[] ? U : never
type C = ElementOf<string[]> // string
type AwaitedOnce<T> = T extends Promise<infer U> ? U : T
type D = AwaitedOnce<Promise<number>> // number
// This is how ReturnType is defined in the standard library:
type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never
type E = MyReturnType<() => User> // User
// Conditional types distribute over unions automatically
type NonNullish<T> = T extends null | undefined ? never : T
type F = NonNullish<string | null | number> // string | number
type User = { id: number; name: string; email?: string }
type User = { id: number; name: string; email?: string }
// Mapped types iterate over the keys of another type
type ReadonlyDeepish<T> = { readonly [K in keyof T]: T[K] }
type Nullable<T> = { [K in keyof T]: T[K] | null }
// Modifiers: + and - add or REMOVE readonly / optional
type Required2<T> = { [K in keyof T]-?: T[K] }
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
type G = Required2<User> // email is no longer optional
// Key remapping with "as" — rename keys while mapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<User>
// { getId: () => number; getName: () => string; getEmail: () => string | undefined }
// Filtering keys: map to never to drop them
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
type H = OnlyStrings<User> // { name: string }
// Template literal types build strings at the type level
type Method = 'get' | 'post' | 'delete'
type Endpoint = 'users' | 'orders'
type Route = `/${Endpoint}` // "/users" | "/orders"
type Handler = `${Method}${Capitalize<Endpoint>}` // "getUsers" | "postUsers" | ...
// Real use: type-safe event names
type EventMap = {
click: { x: number; y: number }
keypress: { key: string }
}
class Emitter {
on<K extends keyof EventMap>(event: K, cb: (payload: EventMap[K]) => void) { /* ... */ }
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) { /* ... */ }
}
const bus = new Emitter()
bus.on('click', p => console.log(p.x)) // p is inferred as { x, y }
// bus.on('clik', ...) -> error: not assignable to keyof EventMap
// bus.emit('keypress', { x: 1 }) -> error: wrong payload shape
// satisfies — check a value against a type WITHOUT widening it
const config = {
port: 3000,
host: 'localhost',
} satisfies Record<string, string | number>
config.port.toFixed(0) // still known to be number, not string | number
Finished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.