Advanced Types
The type-level programming layer: operators that compute new types from existing ones. This is what lets a library describe “the return type of whatever function you passed me” instead of making you write it out.
keyof T produces the union of T’s property names.
interface User { id: number; name: string; email: string;}
type UserKey = keyof User; // 'id' | 'name' | 'email'
let k: UserKey = 'name';k = 'age'; // error: Type '"age"' is not assignable to type 'keyof User'On a type with an index signature you get the index type back:
type Dict = { [key: string]: number };type DictKey = keyof Dict; // string | number
type NumDict = { [key: number]: string };type NumKey = keyof NumDict; // numberkeyof any is string | number | symbol, which is also spelled PropertyKey.
typeof (the type operator)
Section titled “typeof (the type operator)”In a type position, typeof x means “the type of the value x”. This is unrelated to
JavaScript’s runtime typeof, which appears in expression positions.
const defaults = { retries: 3, timeout: 5000, verbose: false,};
type Defaults = typeof defaults;// { retries: number; timeout: number; verbose: boolean }
function apply(overrides: Partial<Defaults>) {}This is the standard way to derive a type from a value you already wrote, so the two can’t drift.
It works on functions and classes too:
function makeUser(name: string, age: number) { return { name, age, createdAt: new Date() };}
type MakeUser = typeof makeUser; // (name: string, age: number) => {...}type User = ReturnType<typeof makeUser>; // { name: string; age: number; createdAt: Date }
class Service {}type ServiceCtor = typeof Service; // the constructor, not an instanceCombined with as const, it turns runtime constants into precise types:
const ROLES = ['admin', 'editor', 'viewer'] as const;type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer'Indexed access types
Section titled “Indexed access types”T[K] is the type of T’s K property, where K can be a union.
interface Product { id: string; price: number; tags: string[];}
type Price = Product['price']; // numbertype IdOrPrice = Product['id' | 'price']; // string | numbertype AnyValue = Product[keyof Product]; // string | number | string[]type Tag = Product['tags'][number]; // string — element type of an array[number] on an array or tuple type gives the element type — the idiom used with as const above.
Conditional types
Section titled “Conditional types”A extends B ? X : Y is a type-level ternary. It asks “is A assignable to B?”.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // truetype B = IsString<42>; // falseDistribution over unions
Section titled “Distribution over unions”When the checked type is a naked type parameter and you pass a union, the conditional distributes across each member and the results are unioned.
type ToArray<T> = T extends unknown ? T[] : never;
type X = ToArray<string | number>; // string[] | number[] (not (string | number)[])That’s the mechanism behind Exclude and Extract. To switch distribution off, wrap both sides
in a tuple:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;type Y = ToArrayNonDist<string | number>; // (string | number)[]infer declares a type variable inside the extends clause and captures whatever matched.
type ElementOf<T> = T extends (infer E)[] ? E : never;type A = ElementOf<string[]>; // stringtype B = ElementOf<number>; // never
type Unwrap<T> = T extends Promise<infer V> ? V : T;type C = Unwrap<Promise<User>>; // Usertype D = Unwrap<string>; // stringYou can infer from function signatures, tuples, and constructors:
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;type MyParameters<F> = F extends (...args: infer P) => any ? P : never;
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never;
type E = Head<[string, number, boolean]>; // stringtype F = Tail<[string, number, boolean]>; // [number, boolean]infer supports constraints (TypeScript 4.7+):
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;Recursive conditional types
Section titled “Recursive conditional types”Conditional types may reference themselves, which enables real computation.
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
type Flatten<T> = T extends readonly (infer E)[] ? Flatten<E> : T;type G = Flatten<number[][][]>; // numberMapped types
Section titled “Mapped types”A mapped type builds a new object type by iterating over keys.
type Optional<T> = { [K in keyof T]?: T[K];};
type Stringify<T> = { [K in keyof T]: string;};
interface User { id: number; name: string }type A = Optional<User>; // { id?: number; name?: string }type B = Stringify<User>; // { id: string; name: string }Modifiers: adding and removing
Section titled “Modifiers: adding and removing”readonly and ? can be added with + (implied) or removed with -.
type Mutable<T> = { -readonly [K in keyof T]: T[K];};
type Concrete<T> = { [K in keyof T]-?: T[K]; // removes optionality AND undefined from the type};
type Frozen<T> = { +readonly [K in keyof T]+?: T[K]; // the + is optional; this is `Readonly<Partial<T>>`};Key remapping with as
Section titled “Key remapping with as”Since TypeScript 4.1, as in a mapped type rewrites each key.
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};
interface User { id: number; name: string }
type UserGetters = Getters<User>;// { getId: () => number; getName: () => string }Mapping a key to never removes it — this is how Omit is implemented:
type RemoveField<T, Field extends keyof T> = { [K in keyof T as K extends Field ? never : K]: T[K];};
type WithoutId = RemoveField<User, 'id'>; // { name: string }Filtering by value type:
type OnlyFunctions<T> = { [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];};
interface Api { url: string; get(): void; post(): void;}type ApiMethods = OnlyFunctions<Api>; // { get(): void; post(): void }You can also map over an arbitrary union, not just keyof T:
type Flags = { [K in 'debug' | 'verbose']: boolean };// { debug: boolean; verbose: boolean }Template literal types
Section titled “Template literal types”String literal types you build with interpolation, added in TypeScript 4.1.
type Greeting = `hello ${string}`;const a: Greeting = 'hello world'; // fineconst b: Greeting = 'goodbye'; // errorInterpolating unions produces the cross product:
type Size = 'sm' | 'md' | 'lg';type Side = 'top' | 'bottom';
type Margin = `m${'t' | 'b'}-${Size}`;// 'mt-sm' | 'mt-md' | 'mt-lg' | 'mb-sm' | 'mb-md' | 'mb-lg'
type CssVar = `--${string}`;Four intrinsic string types are built in:
type A = Uppercase<'hello'>; // 'HELLO'type B = Lowercase<'HELLO'>; // 'hello'type C = Capitalize<'hello'>; // 'Hello'type D = Uncapitalize<'Hello'>; // 'hello'Template literals also work with infer for parsing:
type ExtractParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParams<`/${Rest}`> : Path extends `${string}:${infer Last}` ? Last : never;
type Params = ExtractParams<'/users/:userId/posts/:postId'>;// 'userId' | 'postId'That pattern is how typed routers give you autocomplete on params.userId.
Built-in utility types
Section titled “Built-in utility types”TypeScript ships these in lib.es5.d.ts; they’re globally available.
Object shape utilities
Section titled “Object shape utilities”interface User { id: number; name: string; email?: string;}
type A = Partial<User>;// { id?: number; name?: string; email?: string }
type B = Required<User>;// { id: number; name: string; email: string }
type C = Readonly<User>;// { readonly id: number; readonly name: string; readonly email?: string }
type D = Pick<User, 'id' | 'name'>;// { id: number; name: string }
type E = Omit<User, 'email'>;// { id: number; name: string }
type F = Record<'admin' | 'guest', string[]>;// { admin: string[]; guest: string[] }Practical uses:
// A create payload: server generates the id.type CreateUser = Omit<User, 'id'>;
// A patch payload: every field optional, id required.type UpdateUser = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;
function update(patch: UpdateUser) {}update({ id: 1, name: 'Ada' }); // fineupdate({ name: 'Ada' }); // error: Property 'id' is missingAlso note Partial and Readonly are shallow — nested objects are untouched.
Union utilities
Section titled “Union utilities”type Status = 'idle' | 'loading' | 'success' | 'error';
type A = Exclude<Status, 'error'>; // 'idle' | 'loading' | 'success'type B = Extract<Status, 'success' | 'error'>; // 'success' | 'error'type C = NonNullable<string | null | undefined>; // stringExclude<T, U> removes members of T assignable to U; Extract<T, U> keeps them. Both are
one-line distributive conditionals:
type Exclude<T, U> = T extends U ? never : T;type Extract<T, U> = T extends U ? T : never;Extract is handy for filtering a discriminated union to one variant:
type Action = | { type: 'add'; value: number } | { type: 'remove'; id: string } | { type: 'clear' };
type AddAction = Extract<Action, { type: 'add' }>; // { type: 'add'; value: number }Function utilities
Section titled “Function utilities”function createUser(name: string, age: number) { return { id: 1, name, age };}
type A = ReturnType<typeof createUser>; // { id: number; name: string; age: number }type B = Parameters<typeof createUser>; // [name: string, age: number]type C = Parameters<typeof createUser>[0]; // string
class Service { constructor(public url: string, public timeout: number) {}}type D = ConstructorParameters<typeof Service>; // [url: string, timeout: number]type E = InstanceType<typeof Service>; // ServiceParameters returning a tuple means you can forward arguments with full type safety:
function withLogging<F extends (...args: any[]) => any>(fn: F) { return (...args: Parameters<F>): ReturnType<F> => { console.log('calling', fn.name, args); return fn(...args); };}
const loggedCreate = withLogging(createUser);loggedCreate('Ada', 36); // fully typed; returns { id: number; name: string; age: number }loggedCreate('Ada'); // error: Expected 2 arguments, but got 1Awaited
Section titled “Awaited”Added in TypeScript 4.5. It unwraps promises recursively, matching await’s real behaviour.
type A = Awaited<Promise<string>>; // stringtype B = Awaited<Promise<Promise<number>>>; // numbertype C = Awaited<string>; // string
async function load() { return { id: 1 };}type Loaded = Awaited<ReturnType<typeof load>>; // { id: number }Awaited is the right way to get the resolved type of an async function — ReturnType alone gives
you the Promise<...>.
The rest, briefly
Section titled “The rest, briefly”type A = ThisParameterType<(this: Window, n: number) => void>; // Windowtype B = OmitThisParameter<(this: Window, n: number) => void>; // (n: number) => voidtype C = NoInfer<string>; // TS 5.4+: blocks inference at this positiontype D = Uppercase<'abc'>; // string intrinsics, listed abovesatisfies
Section titled “satisfies”Added in TypeScript 4.9. expr satisfies T checks that expr is assignable to T without
widening the inferred type to T.
The problem it solves:
type Palette = Record<string, string | [number, number, number]>;
// With an annotation, you lose the specifics.const colours: Palette = { red: [255, 0, 0], green: '#00ff00',};colours.green.toUpperCase(); // error: Property 'toUpperCase' does not exist on // type 'string | [number, number, number]'colours.blue; // no error — the index signature allows any key// With `satisfies`, you get both the check and the precise type.const colours = { red: [255, 0, 0], green: '#00ff00',} satisfies Palette;
colours.green.toUpperCase(); // fine — green is `string`colours.red[0]; // fine — red is number[]colours.blue; // error: Property 'blue' does not existsatisfies also catches typos that a bare object literal wouldn’t:
const config = { host: 'localhost', prot: 8080, // typo} satisfies { host: string; port: number };// error: Object literal may only specify known properties, and 'prot' does not existRules of thumb:
| Goal | Use |
|---|---|
| Verify a value and keep its narrow type | satisfies T |
| Verify a value and widen it to the contract | : T annotation |
| Override the compiler with no check | as T (avoid) |
as const
Section titled “as const”A const assertion stops all literal widening and makes everything readonly, recursively.
const a = { x: 1, mode: 'dark' };// { x: number; mode: string }
const b = { x: 1, mode: 'dark' } as const;// { readonly x: 1; readonly mode: 'dark' }
const c = [1, 2, 3]; // number[]const d = [1, 2, 3] as const; // readonly [1, 2, 3]The two workhorse patterns:
// 1. Derive a union from an array of values.const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const;type Method = (typeof METHODS)[number]; // 'GET' | 'POST' | 'PUT' | 'DELETE'
for (const m of METHODS) { /* m: Method */ }
// 2. An enum replacement with runtime values.const Status = { Active: 'active', Banned: 'banned' } as const;type Status = (typeof Status)[keyof typeof Status]; // 'active' | 'banned'as const applies to literal expressions only — object literals, array literals, and primitive
literals. You can’t write as const on a function call result or a variable reference.
Combining as const with satisfies gives you the strongest form: checked and narrow.
const routes = { home: '/', user: '/users/:id',} as const satisfies Record<string, `/${string}`>;
type RoutePath = (typeof routes)[keyof typeof routes]; // '/' | '/users/:id'Key points
Section titled “Key points”keyofgives key unions,typeoflifts a value into a type,T[K]reads a property type.- Conditional types distribute over unions when the checked type is a naked type parameter; wrap in
[ ]to stop it. infercaptures a matched type — the basis ofReturnType,Parameters, and tuple surgery.- Mapped types iterate keys;
-readonly/-?remove modifiers;asremaps keys, and mapping toneverdrops them. - Template literal types build and parse strings; watch the combinatorial explosion.
- Learn
Partial,Pick,Omit,Record,Exclude,Extract,ReturnType,Parameters, andAwaited— they cover most day-to-day needs.Omitdoesn’t validate its keys. satisfieschecks without widening;as constfreezes literals;as const satisfies Tgives you both.