Type Manipulation
|
This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
The type system is a small pure language for computing types from other types. The Types from Types handbook chapter is the map; this page tours each operator with a runnable example. See Generics for the type parameters these operators run on, and Utility Types for the standard library built from them.
Type queries: keyof, typeof, indexed access
keyof T is the union of T’s known public property keys. The type query `typeof x reads the static
type of a value binding — it is a type-position operator, unrelated to the runtime typeof in a JavaScript
expression. An indexed access type T[K] looks up the type stored at key K.
interface User {
id: number;
name: string;
admin: boolean;
}
type UserKey = keyof User; // "id" | "name" | "admin"
type Name = User["name"]; // string -- indexed access with a literal key
type Value = User[keyof User]; // number | string | boolean
const scores = { math: 90, art: 82 };
type Scores = typeof scores; // { math: number; art: number } -- type query
type Subject = keyof typeof scores; // "math" | "art"
const tuple = [1, "a", true] as const;
type Elem = (typeof tuple)[number]; // 1 | "a" | true -- number index on a tuple/array
The key in T[K] can be a literal (User['id']), a union (User['id' | 'name'] gives number | string),
or number to read the element type of an array or tuple.
Conditional types
A conditional type T extends U ? X : Y picks a branch by testing whether T is assignable to U. The
infer keyword, valid only in the extends clause, captures part of the matched type into a fresh type
variable.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<number>; // false
type ElementType<T> = T extends readonly (infer E)[] ? E : T;
type C = ElementType<string[]>; // string
type D = ElementType<number>; // number -- no match, falls through to T
type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never;
type E = ReturnOf<() => Date>; // Date
The full rules — nested conditionals, multiple infer sites, and constrained inference — are in
Conditional Types.
Distributive conditional types
When the checked type is a naked type parameter, the conditional distributes over a union, evaluating once per member and re-uniting the results:
type ToArray<T> = T extends unknown ? T[] : never;
type Dist = ToArray<string | number>; // string[] | number[] -- distributed
// Wrap both sides in a one-tuple to turn distribution OFF:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type NonDist = ToArrayNonDist<string | number>; // (string | number)[]
Distribution is what makes Exclude and NonNullable filter unions member by member. The [T] extends [U]
idiom is the standard way to compare two unions as whole types instead.
Mapped types
A mapped type walks the keys of an existing type — [K in keyof T] — and rebuilds each property. The
Mapped Types chapter is the reference.
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
The ? and readonly modifiers accept an explicit ` or `-` prefix to *add* or *remove* them. A bare
`readonly` or `?` means `+readonly` / `?.
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required2<T> = { [K in keyof T]-?: T[K] };
type Concrete<T> = { -readonly [K in keyof T]-?: T[K] };
interface Cfg { readonly host: string; port?: number }
type C1 = Mutable<Cfg>; // { host: string; port?: number }
type C2 = Required2<Cfg>; // { readonly host: string; port: number }
Key remapping with as
An as clause rewrites each key. Map a key to never and the property is dropped.
type Getters<T> = {
[K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};
interface Point { x: number; y: number }
type PointGetters = Getters<Point>;
// { getX: () => number; getY: () => number }
type RemoveKind<T> = { [K in keyof T as Exclude<K, "kind">]: T[K] };
type Bare = RemoveKind<{ kind: "circle"; r: number }>; // { r: number }
Record<Keys, Value> is the library’s basic mapped type — every key in Keys mapped to the same Value:
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
// { admin: string[]; user: string[]; guest: string[] }
Template literal types
A template literal type interpolates unions into a string pattern and produces every combination as a string-literal type. See Template Literal Types.
type Lang = "en" | "fr";
type Page = "home" | "about";
type Route = `/${Lang}/${Page}`;
// "/en/home" | "/en/about" | "/fr/home" | "/fr/about"
// `infer` inside a template literal parses a string type apart.
type EventName<T extends string> = T extends `on${infer Rest}` ? Lowercase<Rest> : never;
type Ev = EventName<"onClick">; // "click"
The four intrinsic string types — Uppercase, Lowercase, Capitalize, Uncapitalize — are compiler
built-ins with no TypeScript source, and are most useful inside key remapping, as the get$\{Capitalize<K>}
mapping above shows:
type Loud = Uppercase<"hello">; // "HELLO"
type Quiet = Lowercase<"HELLO">; // "hello"
type Title = Capitalize<"hello">; // "Hello"
type Lower = Uncapitalize<"Hello">; // "hello"
never, and testing your types
never is the empty type: no value has it. It vanishes from a union, wins an intersection, and silently
removes conditional and mapped-type entries — which is exactly how filtering idioms work.
type U = never | string; // string
type I = never & string; // never
type Keep<T> = T extends string ? T : never;
type Filtered = Keep<"a" | 1 | "b">; // "a" | "b"
// A remapped key that resolves to `never` deletes the property entirely.
type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };
type S = OnlyStrings<{ a: string; b: number }>; // { a: string }
Because these types are real logic, test them. @ts-expect-error asserts that the next line fails to
compile:
type Eq<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Assert<T extends true> = T;
type _ok = Assert<Eq<Uppercase<"hi">, "HI">>;
// @ts-expect-error -- a string is not a Record of string arrays
const bad: Record<"a", string[]> = "nope";
For a real suite use a dedicated tool: expect-type's
expectTypeOf<X>().toEqualTypeOf<Y>(), or tsd's expectType /
expectError assertions in *.test-d.ts files.
Two habits worth keeping:
-
Pay attention to how a type displays. Hover it in the editor or use the
// ^?twoslash query. A helper such astype Prettify<T> = \{ [K in keyof T]: T[K] } & \{}forces the compiler to flatten an intersection into one readable object literal without changing its meaning. -
Tail-recursive generic types have a depth limit. TypeScript optimizes a conditional type whose result is a direct recursive call with an accumulator, allowing roughly 1000 iterations before the "Type instantiation is excessively deep and possibly infinite" error (
TS2589). Non-tail recursion caps out far sooner.
// Tail-recursive: the recursive call IS the result.
type BuildTuple<N extends number, Acc extends unknown[] = []> =
Acc["length"] extends N ? Acc : BuildTuple<N, [...Acc, unknown]>;
type Ten = BuildTuple<10>["length"]; // 10
// type Huge = BuildTuple<100000>; // error TS2589
See also
-
Generics — type parameters and constraints, where
inferlives. -
Utility Types —
Partial,Pick,Exclude,ReturnType, and the rest, each a thin wrapper over the operators here. -
Objects and Interfaces — the object types these operators transform.
-
The Type System — unions, intersections, and assignability, the rules conditional types query.
-
Types from Types in the handbook ties the whole chapter together.