Type Annotations
Annotate variables, parameters, and return types with a colon suffix.
let name: string = 'Alice';
let age: number = 30;
let active: boolean = true;
let data: unknown = fetch('/api');
function greet(name: string): string {
return `Hello, ${name}!`;
}
const add = (a: number, b: number): number => a + b;
function log(msg: string): void {
console.log(msg);
}
const x = 42;
const y = [1, 2, 3];
Interfaces
Named structural contracts describing the shape of objects and classes.
interface User {
id: number;
name: string;
email?: string;
readonly createdAt: Date;
}
interface Serializable {
serialize(): string;
}
class Config implements Serializable {
private data: Record<string, unknown> = {};
serialize() { return JSON.stringify(this.data); }
}
interface AdminUser extends User {
permissions: string[];
}
interface Dictionary {
[key: string]: string;
}
interface Comparator<T> {
(a: T, b: T): number;
}
Type Aliases
Name any type — primitives, unions, intersections, objects, functions, and complex generics.
type Point = { x: number; y: number };
type ID = string | number;
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type Maybe<T> = T | null | undefined;
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
type Predicate<T> = (value: T) => boolean;
const isEven: Predicate<number> = (n) => n % 2 === 0;
Union Types
A value that can be one of several types — use type guards to work with each member.
type StringOrNumber = string | number;
function format(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
type Status = 'pending' | 'active' | 'inactive';
type Side = 'top' | 'right' | 'bottom' | 'left';
type MaybeString = string | null;
function upper(s: MaybeString): string {
return s?.toUpperCase() ?? '';
}
type Strings = Extract<string | number | boolean, string>;
type NonNulls = NonNullable<string | null | undefined>;
Intersection Types
Combine multiple types — the resulting type has all properties of every member.
type HasName = { name: string };
type HasAge = { age: number };
type HasRole = { role: 'admin' | 'user' };
type AdminUser = HasName & HasAge & HasRole;
const admin: AdminUser = { name: 'Alice', age: 30, role: 'admin' };
type Loggable = { log(): void };
type Serializable = { serialize(): string };
type Service = Loggable & Serializable;
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant: 'primary' | 'secondary';
loading?: boolean;
};
Enums
Named sets of numeric or string constants — const enum inlines values at compile time.
enum Direction { Up, Down, Left, Right }
Direction.Up;
enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Inactive = 'INACTIVE',
}
const enum Color { Red = 'red', Green = 'green', Blue = 'blue' }
function move(dir: Direction) { }
move(Direction.Up);
type Role = 'admin' | 'user' | 'guest';
Tuple Types
Fixed-length arrays with a known type at each position.
type Pair = [number, number];
const p: Pair = [10, 20];
type Range = [start: number, end: number];
type WithOptional = [string, number?];
type OneOrMore = [string, ...string[]];
const [x, y]: Pair = [10, 20];
const [first, ...rest]: OneOrMore = ['a', 'b', 'c'];
function useToggle(): [boolean, () => void] {
let on = false;
return [on, () => { on = !on; }];
}
const [isOpen, toggle] = useToggle();
Type Assertions & Non-null Assertion
Override inferred types when you know more than the compiler — use as sparingly as possible.
const el = document.getElementById('root') as HTMLDivElement;
const user = response.json() as User;
const el2 = document.getElementById('root')!;
el2.style.color = 'red';
const config = { port: 3000, env: 'production' } as const;
const value = (getValue() as unknown) as SpecificType;
function isUser(val: unknown): val is User {
return typeof val === 'object' && val !== null && 'name' in val;
}
Generics
Parameterize functions, interfaces, and classes over types they operate on.
function identity<T>(value: T): T { return value; }
const n = identity(42);
const s = identity('hello');
interface Box<T> {
value: T;
map<U>(fn: (v: T) => U): Box<U>;
}
class Stack<T> {
#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; }
}
function zip<T, U>(a: T[], b: U[]): [T, U][] {
return a.map((item, i) => [item, b[i]]);
}
zip([1, 2], ['a', 'b']);
Generic Constraints
Limit what types a type parameter can be with extends — access only guaranteed properties.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('hello', 'hi');
longest([1, 2, 3], [1, 2]);
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProperty({ name: 'Alice', age: 30 }, 'name');
interface Container<T = string> {
value: T;
}
const c1: Container = { value: 'hello' };
const c2: Container<number> = { value: 42 };
keyof & typeof Operators
keyof extracts a union of property keys; typeof captures the type of a runtime value.
type User = { id: number; name: string; email: string };
type UserKey = keyof User;
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {} as Pick<T, K>);
}
const config = { port: 3000, host: 'localhost', debug: false };
type Config = typeof config;
function fetchUser(id: string) { return { id, name: '' }; }
type FetchReturn = ReturnType<typeof fetchUser>;
const ROUTES = { home: '/', about: '/about', contact: '/contact' } as const;
type Route = (typeof ROUTES)[keyof typeof ROUTES];
Indexed Access Types
Look up the type of a property using T[K] — extract types from nested structures.
type User = { id: number; address: { city: string; zip: string } };
type UserID = User['id'];
type Address = User['address'];
type City = User['address']['city'];
type IdOrName = User['id' | 'name'];
type FruitItem = string[][number];
type Tuple = [string, number, boolean];
type First = Tuple[0];
type AnyElement = Tuple[number];
const routes = [
{ path: '/', component: 'Home' },
] as const;
type RouteItem = (typeof routes)[number];
Utility Types: Object Transformation
Partial, Required, Readonly, Record, Pick, and Omit — reshape object types without repetition.
type User = { id: number; name: string; email: string; role: 'admin' | 'user' };
type UpdateUser = Partial<User>;
type RequiredUser = Required<Partial<User>>;
const user: Readonly<User> = { id: 1, name: 'Alice', email: '', role: 'user' };
type RolePerms = Record<'admin' | 'user', string[]>;
const perms: RolePerms = { admin: ['read', 'write'], user: ['read'] };
type UserSummary = Pick<User, 'id' | 'name'>;
type PublicUser = Omit<User, 'email' | 'role'>;
Utility Types: Unions & Functions
Exclude, Extract, NonNullable, ReturnType, Parameters, and Awaited.
type A = Exclude<'a' | 'b' | 'c', 'a' | 'c'>;
type B = Extract<string | number | boolean, number | boolean>;
type C = NonNullable<string | null | undefined>;
type Fn = (a: string, b: number) => boolean;
type P = Parameters<Fn>;
type R = ReturnType<Fn>;
type D = Awaited<Promise<string>>;
type E = Awaited<Promise<Promise<number>>>;
async function fetchData(): Promise<{ id: number }> { }
type Data = Awaited<ReturnType<typeof fetchData>>;
Type Guards
Narrow a union to a specific member using built-in or user-defined guards.
function process(val: string | number) {
if (typeof val === 'string') val.toUpperCase();
else val.toFixed(2);
}
function handle(err: unknown) {
if (err instanceof TypeError) console.error('Type error:', err.message);
else if (err instanceof Error) console.error('Error:', err.message);
else console.error('Unknown:', err);
}
type Cat = { meow(): void };
type Dog = { bark(): void };
function sound(pet: Cat | Dog) {
if ('meow' in pet) pet.meow();
else pet.bark();
}
function isUser(val: unknown): val is User {
return (
typeof val === 'object' &&
val !== null &&
typeof (val as User).name === 'string'
);
}
const values: unknown[] = getValues();
const users = values.filter(isUser);
Discriminated Unions
A union with a shared literal tag property enabling exhaustive, type-safe branching.
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string; code: number }
| { status: 'loading' };
function render<T>(result: Result<T>): string {
switch (result.status) {
case 'success': return `Data: ${JSON.stringify(result.data)}`;
case 'error': return `Error ${result.code}: ${result.message}`;
case 'loading': return 'Loading...';
default: {
const _exhaustive: never = result;
return _exhaustive;
}
}
}
Mapped Types
Iterate over the keys of a type and transform each property — the basis of all built-in utility types.
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
type StringProps = PickByValue<{ a: string; b: number; c: string }, string>;
Conditional Types
Branch type logic with T extends U ? A : B — distributes over union members by default.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
type B = IsString<number>;
type C = IsString<string | number>;
type IsStringExact<T> = [T] extends [string] ? true : false;
type D = IsStringExact<string | number>;
type Unwrap<T> = T extends Promise<infer R> ? R : T;
type E = Unwrap<Promise<string>>;
type F = Unwrap<number>;
type Flatten<T> = T extends (infer E)[] ? E : T;
type G = Flatten<string[]>;
The infer Keyword
Capture and name a matched sub-type inside a conditional type.
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type R = MyReturnType<(n: number) => boolean>;
type Params<T> =
T extends (...args: infer P) => unknown ? P : never;
type P = Params<(a: string, b: number) => void>;
type Await<T> = T extends Promise<infer V> ? V : T;
type UnpackMap<T> =
T extends Map<infer K, infer V> ? { key: K; value: V } : never;
type KV = UnpackMap<Map<string, number>>;
type NewableReturn<T> =
T extends new (...args: any[]) => infer I ? I : never;
Template Literal Types
TS 4.1Construct new string literal types using template syntax — distributes over union members.
type Direction = 'top' | 'right' | 'bottom' | 'left';
type CSSMargin = `margin-${Direction}`;
type Upper = Uppercase<'hello'>;
type Cap = Capitalize<'world'>;
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: (e: Event) => void;
};
type ClickFocusBlur = EventHandlers<'click' | 'focus' | 'blur'>;
type Column = 'name' | 'age' | 'email';
type OrderBy = `${Column} ASC` | `${Column} DESC`;
type ExtractId<T extends string> =
T extends `${string}_${infer ID}` ? ID : never;
type Id = ExtractId<'user_abc123'>;
Class Modifiers
TypeScript adds access modifiers, readonly, abstract, and override to JavaScript classes.
class Animal {
readonly name: string;
public species: string = '';
protected age: number = 0;
#secret: string = '';
constructor(name: string) { this.name = name; }
}
class Dog extends Animal {
override speak(): void {
console.log(`${this.name} barks!`);
}
birthday() { this.age++; }
}
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
Abstract Classes
Define a base class with methods subclasses must implement — cannot be instantiated directly.
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}, P: ${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private r: number) { super(); }
area() { return Math.PI * this.r ** 2; }
perimeter() { return 2 * Math.PI * this.r; }
}
class Rectangle extends Shape {
constructor(private w: number, private h: number) { super(); }
area() { return this.w * this.h; }
perimeter() { return 2 * (this.w + this.h); }
}
const shapes: Shape[] = [new Circle(5), new Rectangle(3, 4)];
shapes.forEach(s => console.log(s.describe()));
Function Overloads
Declare multiple call signatures to express type-level polymorphism for a single function.
function format(value: string): string;
function format(value: number, decimals?: number): string;
function format(value: Date): string;
function format(value: string | number | Date, decimals = 2): string {
if (typeof value === 'string') return value.trim();
if (typeof value === 'number') return value.toFixed(decimals);
return value.toISOString();
}
format(' hello ');
format(3.14, 2);
format(new Date());
class EventBus {
on(event: 'data', fn: (data: Buffer) => void): this;
on(event: 'end', fn: () => void): this;
on(event: 'error', fn: (e: Error) => void): this;
on(event: string, fn: (...args: any[]) => void): this {
return this;
}
}
as const
Infer the narrowest possible literal type and make the value readonly throughout.
const config1 = { port: 3000, env: 'production' };
const config2 = { port: 3000, env: 'production' } as const;
const tuple = [1, 'hello', true] as const;
const STATUS = {
Pending: 'pending',
Active: 'active',
Inactive: 'inactive',
} as const;
type Status = (typeof STATUS)[keyof typeof STATUS];
satisfies Operator
TS 4.9Validate a value against a type without widening its inferred literal types.
type Config = { host: string; port: number; debug?: boolean };
const c1: Config = { host: 'localhost', port: 3000 };
const c2 = { host: 'localhost', port: 3000 } satisfies Config;
type Palette = Record<string, string | number[]>;
const palette = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Palette;
palette.red.map(c => c / 255);
Index Signatures
Describe objects with dynamic string or number keys.
interface StringMap {
[key: string]: string;
}
const map: StringMap = {};
map.anyKey = 'value';
interface Config {
version: string;
[key: string]: string;
}
interface ArrayLike {
[index: number]: string;
length: number;
}
interface EventHandlers {
[event: `on${string}`]: (...args: unknown[]) => void;
}
type Cache = Record<string, { value: unknown; expiresAt: number }>;
using & await using
TS 5.2Explicit resource management — automatically dispose resources when they leave scope.
class DatabaseConnection implements Disposable {
constructor() { console.log('Connected'); }
query(sql: string) { }
[Symbol.dispose]() { console.log('Closed'); }
}
function runQuery() {
using db = new DatabaseConnection();
return db.query('SELECT * FROM users');
}
class FileHandle implements AsyncDisposable {
async [Symbol.asyncDispose]() {
await this.flush();
await this.close();
}
}
async function processFile() {
await using file = openFile('./data.csv');
return file.read();
}
Module Augmentation & Declaration Merging
Extend third-party module types or add to the global scope without touching their source.
import 'express';
declare module 'express-serve-static-core' {
interface Request {
user?: { id: string; role: 'admin' | 'user' };
requestId: string;
}
}
interface Window {
analytics: { track(event: string, data?: object): void };
}
window.analytics.track('pageview');
declare global {
interface Array<T> {
groupBy<K extends PropertyKey>(fn: (item: T) => K): Record<K, T[]>;
}
}
declare const __DEV__: boolean;
declare module '*.svg' { const url: string; export default url; }
Decorators (TC39 Stage 3)
TS 5.0Declarative annotations that augment classes, methods, fields, and accessors.
function timed(target: unknown, ctx: ClassMethodDecoratorContext) {
return function (this: unknown, ...args: unknown[]) {
const t = performance.now();
const result = (target as Function).apply(this, args);
console.log(`${String(ctx.name)}: ${(performance.now() - t).toFixed(1)}ms`);
return result;
};
}
function positive(_: unknown, ctx: ClassFieldDecoratorContext) {
return (value: number) => {
if (value < 0) throw new RangeError(`${String(ctx.name)} must be positive`);
return value;
};
}
class Counter {
@positive count = 0;
@timed
increment() { this.count++; }
}
const Type Parameters
TS 5.0Infer literal types in generic functions without requiring as const at every call site.
function identity<T>(value: T): T { return value; }
const a = identity(['a', 'b']);
function identity<const T>(value: T): T { return value; }
const b = identity(['a', 'b']);
function defineRoutes<const T extends readonly { path: string }[]>(
routes: T,
): T { return routes; }
const routes = defineRoutes([
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' },
]);
Recursive Types
Types that reference themselves — useful for trees, JSON, deeply nested structures.
type JSONValue =
| null | boolean | number | string
| JSONValue[]
| { [key: string]: JSONValue };
type TreeNode<T> = {
value: T;
children?: TreeNode<T>[];
};
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type Config = { db: { host: string; port: number }; debug: boolean };
type ImmutableConfig = DeepReadonly<Config>;
NoInfer Utility Type
TS 5.4Prevent TypeScript from using a specific argument to infer a type parameter.
function createStore<T>(initial: T, defaultState: T): T { return initial; }
function createStore<T>(initial: T, defaultState: NoInfer<T>): T {
return initial ?? defaultState;
}
createStore({ count: 0 }, { count: 0 });
function withDefault<T>(value: T | undefined, fallback: NoInfer<T>): T {
return value ?? fallback;
}
const n = withDefault<number>(undefined, 0);
Type Predicates & Assertion Functions
Functions that communicate type information back to the caller for narrowing.
function isString(val: unknown): val is string {
return typeof val === 'string';
}
function isUser(val: unknown): val is User {
return typeof val === 'object' && val !== null && 'id' in val;
}
const values: unknown[] = getValues();
const users = values.filter(isUser);
function assertDefined<T>(
val: T | null | undefined,
msg = 'Expected defined value',
): asserts val is T {
if (val == null) throw new Error(msg);
}
let maybeUser: User | null = getUser();
assertDefined(maybeUser, 'User not found');
maybeUser.name;