TypeScript adds static typing to JavaScript — which means catching bugs before runtime. This reference covers every construct you need, from basic types to advanced generic patterns.
Quick reference
The 25 patterns that cover 90% of everyday TypeScript.
| Pattern | What it does |
|---|---|
let x: number = 5 |
Primitive type annotation |
const s: string = "hi" |
String annotation |
let b: boolean = true |
Boolean annotation |
let n: null = null |
Null type |
let u: undefined |
Undefined type |
let a: number[] = [1,2] |
Array type |
let t: [string, number] |
Tuple type |
let o: Record<string, number> |
Object with typed values |
interface Foo { x: number } |
Interface declaration |
type Bar = { x: number } |
Type alias |
type AB = A | B |
Union type |
type AB = A & B |
Intersection type |
function f<T>(x: T): T |
Generic function |
Partial<T> |
All fields optional |
Required<T> |
All fields required |
Readonly<T> |
All fields readonly |
Pick<T, 'a'|'b'> |
Subset of fields |
Omit<T, 'a'> |
Exclude fields |
ReturnType<typeof f> |
Infer return type |
keyof T |
Union of keys |
x is Dog |
Type predicate (type guard) |
as const |
Literal type narrowing |
x! |
Non-null assertion |
x as string |
Type assertion |
satisfies T |
Validate without widening |
Primitive types
// Basic types
let age: number = 30;
let name: string = "Alice";
let active: boolean = true;
let nothing: null = null;
let missing: undefined = undefined;
let any: any = "anything goes"; // avoid — disables type checking
let val: unknown = fetchData(); // prefer unknown over any
// any vs unknown: unknown requires a type check before use
function handleInput(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase()); // OK — narrowed to string
}
}
// never — functions that never return
function fail(msg: string): never {
throw new Error(msg);
}
// void — functions that return undefined
function log(msg: string): void {
console.log(msg);
}
// BigInt and Symbol
let big: bigint = 9007199254740991n;
let sym: symbol = Symbol("id");
// Literal types — restrict to exact values
let direction: "north" | "south" | "east" | "west" = "north";
let statusCode: 200 | 404 | 500 = 200;
Arrays, tuples, and objects
// Array — two equivalent syntaxes
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];
// Readonly array — prevent mutation
const items: readonly number[] = [1, 2, 3];
// items.push(4); ← Error: no push on readonly array
// Tuple — fixed-length array with known types at each position
let pair: [string, number] = ["Alice", 30];
let [person, age] = pair; // destructuring
// Labeled tuple (TS 4.0+)
type Point = [x: number, y: number];
let p: Point = [3, 7];
// Object types
let user: { name: string; age: number } = { name: "Alice", age: 30 };
// Optional fields
let config: { host: string; port?: number } = { host: "localhost" };
// Index signatures — objects with dynamic keys
let scores: { [key: string]: number } = {};
scores["Alice"] = 10;
// Record<K, V> — cleaner index signature
let lookup: Record<string, number> = { a: 1, b: 2 };
Interfaces
interface User {
id: number;
name: string;
email?: string; // optional
readonly createdAt: Date; // read-only after creation
}
// Method signatures
interface Greeter {
greet(name: string): string;
greet(name: string, greeting: string): string; // overload
}
// Extending interfaces
interface Admin extends User {
role: "admin" | "superadmin";
permissions: string[];
}
// Extending multiple interfaces
interface SuperAdmin extends Admin, Auditable {
auditLog: string[];
}
// Interface vs type alias:
// - interface: can be reopened/merged, better for OOP/classes
// - type: can use unions/intersections, better for complex types
Type aliases and union/intersection types
// Type alias
type ID = string | number;
type Callback = (event: MouseEvent) => void;
// Union — one of several types
type Result<T> = { data: T; error: null } | { data: null; error: Error };
// Discriminated union — each branch has a literal tag field
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.width * s.height;
}
}
// Intersection — combine all fields
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // { name: string; age: number }
// Type narrowing with typeof
function format(x: string | number): string {
if (typeof x === "string") return x.toUpperCase();
return x.toFixed(2);
}
Functions
// Named function
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => a * b;
// Optional and default parameters
function greet(name: string, greeting = "Hello"): string {
return `${greeting}, ${name}!`;
}
// Rest parameters
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// Overloads — different call signatures
function stringify(x: number): string;
function stringify(x: boolean): string;
function stringify(x: number | boolean): string {
return String(x);
}
// Function type
type Transformer<T, U> = (input: T) => U;
const toStr: Transformer<number, string> = (n) => String(n);
Generics
// Generic function
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // explicit
identity(42); // inferred
// Generic interface
interface Repository<T> {
findById(id: string): Promise<T>;
save(entity: T): Promise<void>;
delete(id: string): Promise<void>;
}
// 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[this.items.length - 1]; }
}
// Constraints — T must have certain shape
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// keyof constraint — only valid keys
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Default generic parameter (TS 2.3+)
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
Utility types
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial<T> — make all fields optional
type UserInput = Partial<User>;
// { id?: number; name?: string; email?: string; password?: string }
// Required<T> — make all fields required
type StrictUser = Required<UserInput>;
// Readonly<T> — prevent mutation
type ImmutableUser = Readonly<User>;
// const u: ImmutableUser = {...}; u.name = "x"; ← Error
// Pick<T, K> — select a subset of fields
type PublicUser = Pick<User, "id" | "name" | "email">;
// Omit<T, K> — exclude fields
type SafeUser = Omit<User, "password">;
// Exclude<T, U> — remove from union
type NumOrStr = number | string | boolean;
type NumOrBool = Exclude<NumOrStr, string>; // number | boolean
// Extract<T, U> — keep only matching union members
type OnlyStr = Extract<NumOrStr, string>; // string
// NonNullable<T> — remove null and undefined
type Value = NonNullable<string | null | undefined>; // string
// ReturnType<T> — infer function return type
function fetchUser() { return { id: 1, name: "Alice" }; }
type FetchedUser = ReturnType<typeof fetchUser>; // { id: number; name: string }
// Parameters<T> — infer parameter tuple
type FetchParams = Parameters<typeof fetchUser>; // []
// Awaited<T> — unwrap Promise (TS 4.5+)
type Resolved = Awaited<Promise<string>>; // string
// Record<K, V> — typed object
type PageMap = Record<"home" | "about" | "contact", string>;
Enums
// Numeric enum (default, starts at 0)
enum Direction {
North, // 0
South, // 1
East, // 2
West, // 3
}
let dir: Direction = Direction.North;
// String enum — preferred, values are meaningful
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
// Const enum — inlined at compile time (no runtime object)
const enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
// Better alternative to enums: const object + type
const DIRECTION = {
North: "north",
South: "south",
} as const;
type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
// "north" | "south"
Type guards and narrowing
// typeof guard
function process(x: string | number) {
if (typeof x === "string") {
return x.trim(); // x is string here
}
return x.toFixed(2); // x is number here
}
// instanceof guard
function handle(err: Error | string) {
if (err instanceof Error) {
return err.message; // Error
}
return err; // string
}
// in guard — check for property existence
type Cat = { meow(): void };
type Dog = { bark(): void };
function makeNoise(animal: Cat | Dog) {
if ("meow" in animal) {
animal.meow();
} else {
animal.bark();
}
}
// User-defined type predicate
function isString(x: unknown): x is string {
return typeof x === "string";
}
// Discriminated union narrowing
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
function unwrap<T>(r: Result<T>): T {
if (r.ok) return r.value;
throw new Error(r.error);
}
// Assertion function (TS 3.7+)
function assert(condition: boolean, msg: string): asserts condition {
if (!condition) throw new Error(msg);
}
Classes
class Animal {
// Fields
readonly species: string;
protected name: string;
private #age: number; // private field (hard private, ES2022)
constructor(species: string, name: string, age: number) {
this.species = species;
this.name = name;
this.#age = age;
}
// Getter/setter
get age(): number { return this.#age; }
set age(v: number) {
if (v < 0) throw new Error("negative age");
this.#age = v;
}
// Method
describe(): string {
return `${this.name} (${this.species})`;
}
// Static
static create(species: string, name: string): Animal {
return new Animal(species, name, 0);
}
}
class Dog extends Animal {
constructor(name: string) {
super("Canis lupus", name, 0);
}
// Override
describe(): string {
return `Dog: ${super.describe()}`;
}
}
// Implementing an interface
interface Serializable {
serialize(): string;
}
class User implements Serializable {
constructor(public id: number, public name: string) {}
serialize() { return JSON.stringify(this); }
}
// Abstract class
abstract class Shape {
abstract area(): number;
toString() { return `Area: ${this.area()}`; }
}
Mapped types and conditional types
// Mapped type — transform all properties
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Mapped type with remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Conditional type
type IsString<T> = T extends string ? true : false;
type R1 = IsString<"hello">; // true
type R2 = IsString<number>; // false
// Infer in conditional types
type ElementType<T> = T extends (infer E)[] ? E : never;
type Item = ElementType<string[]>; // string
// Distributive conditional types
type Flatten<T> = T extends any[] ? T[number] : T;
type F1 = Flatten<number[]>; // number
type F2 = Flatten<string>; // string
// Template literal types (TS 4.1+)
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
Common patterns
// Builder pattern
class QueryBuilder {
private query = "";
select(fields: string[]): this {
this.query += `SELECT ${fields.join(", ")} `;
return this;
}
from(table: string): this {
this.query += `FROM ${table} `;
return this;
}
where(condition: string): this {
this.query += `WHERE ${condition} `;
return this;
}
build(): string { return this.query.trim(); }
}
const q = new QueryBuilder()
.select(["id", "name"])
.from("users")
.where("active = true")
.build();
// Branded types — prevent mixing similar primitives
type UserId = string & { readonly _brand: "UserId" };
type OrderId = string & { readonly _brand: "OrderId" };
function makeUserId(id: string): UserId { return id as UserId; }
// function takesUser(id: UserId) {}
// takesUser("raw-string"); ← Error: string is not UserId
// satisfies operator (TS 4.9+) — validate shape without widening
const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | number[]>;
palette.red.map(v => v * 2); // OK — type preserved as number[]
6 common mistakes
1. Using any instead of unknown
// BAD — disables type checking
function parse(input: any) { return input.toUpperCase(); } // no error even if input is number
// GOOD — forces type narrowing
function parse(input: unknown) {
if (typeof input !== "string") throw new Error("expected string");
return input.toUpperCase();
}
2. Type assertion instead of type guard
// BAD — bypasses type safety
const user = getData() as User; // crashes if getData() returns null
// GOOD — check first
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null && "id" in x;
}
if (isUser(data)) { /* safe */ }
3. Forgetting readonly on tuples
// BAD — tuple can be mutated
function min(...nums: number[]) {
return nums.sort()[0]; // mutates original array!
}
// GOOD — readonly prevents mutation
function min(...nums: readonly number[]) {
return [...nums].sort()[0];
}
4. enum runtime overhead
// BAD — generates runtime object, easy to misuse
enum Status { Active = "ACTIVE" }
// GOOD — zero runtime cost, better inference
const Status = { Active: "ACTIVE" } as const;
type Status = typeof Status[keyof typeof Status];
5. Ignoring strict mode
// tsconfig.json — always enable strict
{
"compilerOptions": {
"strict": true, // enables all strict checks
"noUncheckedIndexedAccess": true, // arr[0] is T | undefined
"exactOptionalPropertyTypes": true // undefined !== missing
}
}
6. Not using satisfies for config objects
// BAD — type widened, loses specific types
const config: Record<string, unknown> = {
port: 3000,
host: "localhost",
};
config.port // unknown — lost number info
// GOOD — validated but types preserved
const config = {
port: 3000,
host: "localhost",
} satisfies Record<string, unknown>;
config.port // number — preserved!
6 frequently asked questions
What's the difference between interface and type?
Use interface when defining object shapes (especially for OOP/classes) — it can be reopened and merged across declaration. Use type when you need unions, intersections, or mapped types. In practice, either works for most cases; pick one and be consistent.
When should I use unknown vs any?
Always prefer unknown. Both accept any value, but unknown requires you to narrow the type before using it (with typeof, instanceof, or a type guard). any silently bypasses the type system. Use any only as a last resort, e.g., migrating JavaScript.
What does as const do?
It converts an object or array to a deeply readonly literal type. const colors = ["red", "green"] as const makes colors readonly ["red", "green"] instead of string[]. This lets you derive union types from arrays: type Color = typeof colors[number] → "red" | "green".
How do I type an async function?
The return type is Promise<T> where T is the resolved value:
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<User>;
}
What is keyof used for?
keyof T produces a union of all keys of type T as string literals. It's most useful in generic functions to ensure you're accessing valid keys:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
get(user, "name"); // OK
get(user, "foo"); // Error: "foo" is not a key of User
How do I extend a third-party module's types?
Use declaration merging in a .d.ts file:
// types/express.d.ts
declare module "express-serve-static-core" {
interface Request {
user?: User; // add user field to every Request
}
}