// Generic function: T is constrained to types with a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest("hello", "hi")); // "hello"
console.log(longest([1, 2, 3], [4, 5])); // [1, 2, 3]
// longest(10, 20); // Error: number has no length
// Generic with default
function createPair<T = string, U = number>(a: T, b: U): [T, U] {
return [a, b];
}
const p1 = createPair("hello", 42); // [string, number]
const p2 = createPair<boolean, Date>(true, new Date());
// Constrained key lookup
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, active: true };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
// getProperty(user, "missing"); // Error: not a key of typeof user
TypeScriptutility_types.tsCopyCopy
#9654; Try it
Built-in utility types
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial: all properties optional — for partial updates
type UserUpdate = Partial<User>;
// Required: all properties required — reverse of Partial
type UserRequired = Required<User>;
// Pick: select specific properties
type PublicUser = Pick<User, "id" | "name" | "email">;
// Omit: exclude specific properties
type UserWithoutPassword = Omit<User, "password">;
// Readonly: prevent mutation
const frozenUser: Readonly<User> = {
id: 1, name: "Alice", email: "a@b.c",
password: "hash", createdAt: new Date()
};
// frozenUser.name = "Bob"; // Error: readonly
// Record: key-value map type
type RolePermissions = Record<"admin" | "user" | "guest", string[]>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// ReturnType, Parameters
function fetchUser(id: number, format: "json" | "xml"): Promise<User> {
return Promise.resolve({} as User);
}
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [number, "json"|"xml"]
TypeScriptdiscriminated_unions.tsCopyCopy
#9654; Try it
Discriminated unions for typed state
// Discriminated union: common "kind" field narrows the type
type NetworkState =
| { kind: "idle" }
| { kind: "loading"; requestId: string }
| { kind: "success"; data: unknown; timestamp: Date }
| { kind: "error"; error: Error; retryCount: number };
function handleState(state: NetworkState): string {
switch (state.kind) {
case "idle": return "Ready";
case "loading": return `Loading (${state.requestId})`;
case "success": return `Done at ${state.timestamp.toISOString()}`;
case "error": return `Failed: ${state.error.message} (${state.retryCount} retries)`;
// TypeScript warns if any case is missing — exhaustiveness check
}
}
// Exhaustiveness helper: errors at compile time if a case is missed
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
// Result type pattern
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: "Division by zero" };
return { ok: true, value: a / b };
}
const r = divide(10, 2);
if (r.ok) console.log(r.value); // 5 — type narrowed to {ok:true, value:number}
else console.log(r.error); // narrowed to {ok:false, error:string}
TypeScripttype_guards.tsCopyCopy
#9654; Try it
Type guards and narrowing
// User-defined type guard: predicate x is T
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: unknown): x is { name: string; age: number } {
return (
typeof x === "object" && x !== null &&
"name" in x && typeof (x as any).name === "string" &&
"age" in x && typeof (x as any).age === "number"
);
}
// Narrowing with in operator
type Dog = { breed: string; bark(): void };
type Cat = { indoor: boolean; meow(): void };
type Pet = Dog | Cat;
function speak(pet: Pet) {
if ("bark" in pet) {
pet.bark(); // TypeScript knows it's Dog here
} else {
pet.meow(); // TypeScript knows it's Cat here
}
}
// instanceof narrowing
function formatError(e: unknown): string {
if (e instanceof Error) return e.message; // Error
if (typeof e === "string") return e; // string
if (typeof e === "object" && e !== null) return JSON.stringify(e);
return String(e);
}
// Assertion functions (TypeScript 3.7+)
function assert(condition: unknown, msg: string): asserts condition {
if (!condition) throw new Error(msg);
}
function processUser(user: User | null) {
assert(user !== null, "User must not be null");
console.log(user.name); // user is narrowed to User here
}
TypeScriptmapped_types.tsCopyCopy
#9654; Try it
Mapped types and template literal types
// Mapped type: transform each property in a type
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // remove readonly
// Conditional mapped type: change value type based on condition
type Serialize<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K];
};
interface Event {
id: number;
name: string;
startDate: Date;
endDate: Date;
}
type SerializedEvent = Serialize<Event>;
// { id: number; name: string; startDate: string; endDate: string }
// Template literal types (TypeScript 4.1+)
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Build getter/setter pairs
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void;
};
type UserAccessors = Getters<Pick<User, "name" | "email">> &
Setters<Pick<User, "name" | "email">>;
// { getName: () => string; getEmail: () => string;
// setName: (v:string)=>void; setEmail: (v:string)=>void }
TypeScriptconditional_types.tsCopyCopy
#9654; Try it
Conditional types and infer
// Conditional type: T extends U ? X : Y
type IsArray<T> = T extends any[] ? true : false;
type IsString<T> = T extends string ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // false
// infer: extract a type from within another type
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type UnpackPromise<T> = T extends Promise<infer R> ? R : T;
type UnpackFunction<T> = T extends (...args: any[]) => infer R ? R : never;
type C = UnpackArray<string[]>; // string
type D = UnpackPromise<Promise<User>>; // User
type E = UnpackFunction<() => number>; // number
// Distributive conditional types: apply to each union member
type NonNullable<T> = T extends null | undefined ? never : T;
type Clean = NonNullable<string | null | undefined | number>; // string | number
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Config {
database: { host: string; port: number };
features: string[];
}
const cfg: DeepReadonly<Config> = {
database: { host: "localhost", port: 5432 },
features: ["auth", "logging"],
};
// cfg.database.port = 3000; // Error: readonly
TypeScriptinterfaces_vs_types.tsCopyCopy
#9654; Try it
Interface vs type alias — when to use each
// Interface: extensible — supports declaration merging
interface Animal {
name: string;
eat(): void;
}
interface Animal { // merges with first declaration — useful for augmenting libs
sleep(): void;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
// Type alias: more powerful — supports unions, intersections, primitives
type ID = string | number;
type Callback = (err: Error | null, result?: unknown) => void;
type StringOrArray = string | string[];
// Intersection: combine multiple types
type AdminUser = User & { permissions: string[]; superAdmin: boolean };
// Rules of thumb:
// Use interface for: objects, classes, public API shapes, extensible definitions
// Use type for: unions, intersections, primitives, computed types, tuples
// Tuple types: fixed-length arrays with typed positions
type Pair<T, U> = [T, U];
type RGB = [r: number, g: number, b: number]; // labelled tuple
const color: RGB = [255, 128, 0];
const [r, g, b] = color;
// Rest in tuples (variadic tuple types)
type StringsAndNumber = [...string[], number];
const s: StringsAndNumber = ["a", "b", 42];
TypeScriptclasses_decorators.tsCopyCopy
#9654; Try it
Classes with access modifiers and decorators
// TypeScript access modifiers
class BankAccount {
readonly id: string;
private _balance: number;
protected owner: string; // accessible in subclasses
constructor(owner: string, initialBalance: number) {
this.id = crypto.randomUUID();
this.owner = owner;
this._balance = initialBalance;
}
// Parameter shorthand: public/private in constructor
// constructor(public readonly id: string, private _balance: number) {}
get balance(): number { return this._balance; }
deposit(amount: number): this { // return this for method chaining
if (amount <= 0) throw new RangeError("Amount must be positive");
this._balance += amount;
return this;
}
}
// Abstract class: cannot be instantiated directly
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string { return `Area: ${this.area().toFixed(2)}`; }
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area(): number { return Math.PI * this.radius ** 2; }
perimeter(): number { return 2 * Math.PI * this.radius; }
}
// Class implements interface
interface Serializable {
toJSON(): object;
fromJSON(data: object): void;
}
class Config implements Serializable {
constructor(private data: Record<string, unknown> = {}) {}
toJSON() { return { ...this.data }; }
fromJSON(d: object) { Object.assign(this.data, d); }
}
TypeScriptbranded_types.tsCopyCopy
#9654; Try it
Branded types for type-safe IDs
// Branded type: wrap a primitive with a phantom type tag
// Prevents accidentally mixing UserId with OrderId even though both are strings
type Brand<T, B> = T & { readonly _brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
// Constructors / smart constructors validate and brand
function UserId(id: string): UserId {
if (!id.startsWith("usr_")) throw new Error("Invalid UserId");
return id as UserId;
}
function Email(email: string): Email {
if (!email.includes("@")) throw new Error("Invalid email");
return email as Email;
}
function getUser(id: UserId): void {
console.log("Fetching user:", id);
}
const uid = UserId("usr_123");
const oid = "ord_456" as OrderId;
getUser(uid); // OK
// getUser(oid); // Error: OrderId is not UserId
// getUser("usr_789"); // Error: string is not UserId
// Nominal pattern for validated values
type PositiveNumber = Brand<number, "PositiveNumber">;
function toPositive(n: number): PositiveNumber {
if (n <= 0) throw new RangeError("Must be positive");
return n as PositiveNumber;
}
TypeScriptasync_types.tsCopyCopy
#9654; Try it
Async patterns with typed errors
// Typed async result — avoids thrown exceptions crossing async boundaries
type AsyncResult<T, E = Error> = Promise<
| { success: true; data: T }
| { success: false; error: E }
>;
async function safeFetch<T>(url: string): AsyncResult<T> {
try {
const res = await fetch(url);
if (!res.ok) {
return { success: false, error: new Error(`HTTP ${res.status}`) };
}
const data = await res.json() as T;
return { success: true, data };
} catch (err) {
return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
}
}
interface User { id: number; name: string }
async function loadUser(id: number) {
const result = await safeFetch<User>(`/api/users/${id}`);
if (result.success) {
console.log(result.data.name); // data is User — fully typed
} else {
console.error(result.error.message);
}
}
// Awaited: unwrap Promise type
type Resolved = Awaited<Promise<User>>; // User
type Deep = Awaited<Promise<Promise<string>>>; // string
TypeScriptdeclaration_merging.tsCopyCopy
#9654; Try it
Module augmentation and declaration merging
// Extend an existing library's types without modifying source
// Example: add custom property to Express Request
declare global {
namespace Express {
interface Request {
user?: { id: string; roles: string[] };
requestId: string;
}
}
}
// Augment a module
declare module "some-library" {
interface Config {
customOption?: boolean;
}
}
// Extend built-in types
interface Array<T> {
first(): T | undefined;
last(): T | undefined;
}
Array.prototype.first = function() { return this[0]; };
Array.prototype.last = function() { return this[this.length - 1]; };
// const type assertion: narrow literal types
const config = {
endpoint: "https://api.example.com",
method: "GET",
retries: 3,
} as const;
// config.method is "GET" (literal), not string
// config.retries is 3 (literal), not number
// All properties are readonly
type ConfigMethod = typeof config.method; // "GET"
TypeScriptsatisfies.tsCopyCopy
#9654; Try it
satisfies operator (TypeScript 4.9)
// satisfies: validate type without widening — keeps literal types
type Color = "red" | "green" | "blue" | `#${string}`;
type Palette = Record<string, Color | Color[]>;
// Without satisfies: type is widened to Palette — can't use string methods
const palette1: Palette = {
red: "#ff0000",
green: "green",
multicolor: ["red", "blue"],
};
// palette1.red.toUpperCase(); // Error: Color might be Color[]
// With satisfies: validates against Palette but keeps precise types
const palette2 = {
red: "#ff0000", // type: "#ff0000" (literal), checked against Color
green: "green", // type: "green"
multicolor: ["red", "blue"], // type: string[]
} satisfies Palette;
palette2.red.toUpperCase(); // OK — TypeScript knows it's string
palette2.multicolor.map(c => c); // OK — TypeScript knows it's array
// Another use: const with type checking
const routes = {
"/": { component: "Home", auth: false },
"/dashboard": { component: "Dashboard", auth: true },
} satisfies Record<string, { component: string; auth: boolean }>;
// routes["/"].component is string (not widened to unknown)