Types, Interfaces, Generics, Utility Types, Decorators, Mapped Types — type-safe code.
// TypeScript primitive types
let username: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notAssigned: undefined = undefined;
// Special types
let notReturn: void; // function returns nothing
let impossible: never; // never reachable (throw, infinite loop)
let anything: unknown; // safe version of any (requires type narrowing)
let escapeHatch: any; // disables type checking entirely
// BigInt & Symbol
let big: bigint = 9007199254740991n;
let sym: symbol = Symbol("unique");
let globalSym: symbol = Symbol.for("app.global");// Explicit type annotations
const name: string = "Bob";
const coordinates: [number, number] = [12.5, -47.3];
const data: Record<string, unknown> = { key: "value" };
// Type inference — TS infers types automatically
const inferred = "hello"; // inferred as "hello" (literal type)
const inferredNum = 42; // inferred as 42 (literal type)
let mutable = "hello"; // inferred as string (not literal)
// Block-scoped const inference
const colors = ["red", "green", "blue"] as const;
// colors: readonly ["red", "green", "blue"] — no mutations allowed// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];
let mixed: (string | number)[] = [1, "two", 3];
// Tuples — fixed-length, fixed-position types
let pair: [string, number] = ["age", 30];
let named: [name: string, age: number] = ["Alice", 25];
let optional: [string, number?] = ["only-name"]; // optional element
// Nested tuples
let matrix: [number, number][] = [[1, 2], [3, 4]];
// Numeric enums
enum Direction {
Up = 0,
Down,
Left,
Right,
}
// String enums
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
// Const enum (inlined at compile time)
const enum Permissions {
Read,
Write,
Admin,
}
const perm = Permissions.Read; // compiles to 0// Literal types
let direction: "left" | "right" = "left";
let level: 1 | 2 | 3 = 2;
let status: "open" | "closed" | "pending" = "pending";
// Union types — one of many
type ID = string | number;
function printId(id: ID): void {
console.log(typeof id === "string" ? id.toUpperCase() : id);
}
// Intersection types — all combined
type Name = { name: string };
type Age = { age: number };
type Person = Name & Age;
const person: Person = { name: "Alice", age: 30 };
// Type narrowing with unions
function wrapInArray(value: string | string[]): string[] {
if (Array.isArray(value)) return value;
return [value]; // TS knows value is string here
}// Const assertions (as const)
// Locks down literal types & makes everything readonly
// Without as const
let config = {
api: "https://api.example.com",
port: 3000,
features: ["auth", "logging"],
};
// config: { api: string; port: number; features: string[] }
// With as const
let frozen = {
api: "https://api.example.com" as const,
port: 3000 as const,
features: ["auth", "logging"] as const,
};
// frozen.api: "https://api.example.com"
// frozen.port: 3000
// frozen.features: readonly ["auth", "logging"]
// Full object as const
const obj = { x: 10, y: [20, 30] } as const;
// obj.x: 10, obj.y: readonly [20, 30]
// Common use case: function parameter types
type Route = typeof frozen.features[number];
// Route = "auth" | "logging"| Feature | any | unknown |
|---|---|---|
| Type safety | None — escapes checker | Safe — requires narrowing |
| Assignment | Can assign to any type | Can only assign to any/unknown |
| Operations | All operations allowed | No operations without narrowing |
| Best practice | Avoid in strict code | Use over any always |
| typeof check | Not needed | Required before use |
unknown over any. Use any only as a last resort during migration or with explicitly untyped third-party libraries.as const or explicit literal annotations to get precise types from constants. This prevents accidental widening to string, number, etc.// Interface declaration
interface User {
readonly id: number; // cannot be reassigned after creation
name: string;
email: string;
avatar?: string; // optional property
role: "admin" | "editor" | "viewer";
}
// Using the interface
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
role: "admin",
};
// Extending interfaces
interface Employee extends User {
employeeId: string;
department: string;
startDate: Date;
}
const employee: Employee = {
id: 2,
name: "Bob",
email: "bob@company.com",
role: "editor",
employeeId: "EMP-001",
department: "Engineering",
startDate: new Date("2024-01-15"),
};// Type aliases
type Point = { x: number; y: number };
type Callback = (data: string) => void;
type Nullable<T> = T | null;
type Status = "active" | "inactive" | "pending";
// Union & intersection with type aliases
type Timestamped = { createdAt: Date; updatedAt: Date };
type Entity = Point & Timestamped;
// Entity: { x: number; y: number; createdAt: Date; updatedAt: Date }
// Conditional types in aliases
type Unwrap<T> = T extends Array<infer U> ? U : T;
type A = Unwrap<string[]>; // string
type B = Unwrap<string>; // string
// Mapped types
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
// Template literal types
type EventName = `on${Capitalize<string>}`;
type CSSProperty = `--${string}`;// Index signatures — dynamic property keys
interface Dictionary<T> {
[key: string]: T;
}
const ages: Dictionary<number> = {
alice: 30,
bob: 25,
};
// Numeric index signatures
interface NumberMap<T> {
[index: number]: T;
}
const list: NumberMap<string> = ["a", "b", "c"];
// Readonly index signatures
interface ReadonlyDict<T> {
readonly [key: string]: T;
}
// Symbol keys
interface SymbolMap<T> {
[key: symbol]: T;
}// Declaration merging — interfaces only (NOT type aliases)
interface Window {
myCustomProp: string;
}
interface Window {
myOtherProp: number;
}
// Window now has both: myCustomProp + myOtherProp
declare const window: Window & typeof globalThis;
window.myCustomProp = "hello";
window.myOtherProp = 42;
// Merging with function interfaces
interface MyLib {
(input: string): number;
}
interface MyLib {
version: string;
config: Record<string, unknown>;
}
const myLib = ((input: string) => input.length) as MyLib;
myLib.version = "1.0.0";
myLib.config = { debug: true };
// Namespace merging
interface Validators {
isEmail: (s: string) => boolean;
}
namespace Validators {
export const isEmail = (s: string): boolean =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
}| Feature | interface | type |
|---|---|---|
| Extends / Intersection | extends | & (intersection) |
| Union types | Not supported | A | B supported |
| Declaration merging | Yes — auto-merged | No — error on duplicate |
| Mapped types | No | Yes |
| Conditional types | No | Yes |
| Tuple / primitive | No | Yes (type Name = string) |
| implements in class | Yes | No (use type alias) |
| Tooling | Better error messages | More flexible |
interface for object shapes that need extending or merging. Use type for unions, intersections, mapped types, and anything requiring type-level computation.// Basic generic function
function identity<T>(value: T): T {
return value;
}
const result = identity("hello"); // result: string
// Generic arrow function
const getFirst = <T>(arr: T[]): T | undefined => arr[0];
// Multiple type parameters
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b } as T & U;
}
const merged = merge({ name: "Alice" }, { age: 30 });
// merged: { name: string } & { age: number }
// Constrained generic with extends
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // 5 — string has .length
getLength([1, 2, 3]); // 3 — array has .length
getLength({ length: 10 }); // 10 — any object with .length
// getLength(123); // Error: number has no .length// Generic class
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): readonly T[] {
return this.items;
}
filter(predicate: (item: T) => boolean): T[] {
return this.items.filter(predicate);
}
}
const stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");
// Generic interface
interface Repository<T, ID = number> {
findById(id: ID): Promise<T | undefined>;
findAll(): Promise<T[]>;
create(entity: Omit<T, "id">): Promise<T>;
update(id: ID, data: Partial<T>): Promise<T>;
delete(id: ID): Promise<boolean>;
}
// Generic with default type parameter
interface ApiResponse<T, E = Error> {
data: T;
error?: E;
status: number;
}// keyof — get union of an object's keys
type User = { id: number; name: string; email: string };
type UserKey = keyof User; // "id" | "name" | "email"
// Use keyof in generic functions
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", email: "alice@example.com" };
const name = getProperty(user, "name"); // string
const id = getProperty(user, "id"); // number
// getProperty(user, "age"); // Error: "age" not in keyof User
// Mapped types with generics
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends string, T> = { [P in K]: T };
// Practical: typed event emitter keys
type Events = {
login: { userId: number };
logout: void;
error: { message: string; code: number };
};
type EventHandler<T extends keyof Events> = Events[T] extends void
? () => void
: (payload: Events[T]) => void;// Conditional types with generics
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Distributive conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null | undefined>; // string
// infer keyword in conditional types
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type D = ReturnType<() => string>; // string
type E = ReturnType<() => number[]>; // number[]
type ElementType<T> = T extends (infer U)[] ? U : T;
type F = ElementType<string[]>; // string
type G = ElementType<number>; // number
// infer with Promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type H = Awaited<Promise<string>>; // string
type I = Awaited<Promise<Promise<number>>>; // number
// infer in template literal types
type ExtractRoute<T extends string> =
T extends `:param/${infer Rest}` ? Rest : never;
type J = ExtractRoute<":param/users">; // "users"| Constraint | Syntax | Use Case |
|---|---|---|
| extends | T extends SomeType | Limit T to a subtype |
| keyof | K extends keyof T | Key must exist on T |
| default | <T = string> | Fallback type parameter |
| infer | T extends ... infer U | Extract a type from within T |
| new() | T extends new (...args) => U | T must be constructable |
| extends keyof | K extends keyof T | Type-safe property access |
TEntity, TKey) and single letters for simple cases (T, U). Always constrain generics with extends when you know the shape.interface Todo {
title: string;
description: string;
done: boolean;
}
// Partial — all properties become optional
type PartialTodo = Partial<Todo>;
const draft: PartialTodo = { title: "New task" }; // only title needed
// Required — all properties become required (inverts optional)
type RequiredTodo = Required<Todo>;
// Readonly — all properties become readonly
type FrozenTodo = Readonly<Todo>;
const frozen: FrozenTodo = {
title: "Task",
description: "Desc",
done: false,
};
// frozen.done = true; // Error: cannot assign to readonly
// Deep Readonly — recursive
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
const config: DeepReadonly<{ db: { host: string; port: number } }> = {
db: { host: "localhost", port: 5432 },
};
// config.db.host = "remote"; // Error// Record — construct type with keys of K and values of T
type PageInfo = Record<"title" | "url", string>;
const page: PageInfo = {
title: "Home",
url: "/home",
};
type UserRoles = Record<string, "admin" | "editor" | "viewer">;
const roles: UserRoles = {
alice: "admin",
bob: "editor",
};
// Pick — select a subset of properties
type TodoPreview = Pick<Todo, "title" | "done">;
const preview: TodoPreview = {
title: "Buy milk",
done: false,
};
// Omit — exclude specific properties
type TodoInfo = Omit<Todo, "done">;
const info: TodoInfo = {
title: "Write tests",
description: "Unit tests for auth module",
};
// Deep Omit — recursive omit by key
type DeepOmit<T, K extends string> = T extends object
? { [P in keyof T as P extends K ? never : P]: DeepOmit<T[P], K> }
: T;
type WithoutMeta = DeepOmit<{ meta: {}; data: { meta: {}; value: string } }, "meta">;
// { data: { value: string } }// Exclude — remove types from a union
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<string | number | (() => void), Function>; // string | number
// Extract — select types from a union
type T2 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T3 = Extract<string | number, number>; // number
// NonNullable — remove null and undefined from a type
type T4 = NonNullable<string | null | undefined>; // string
type T5 = NonNullable<string[] | null>; // string[]
// Practical use: filter types
type APIResponse<T> = { data: T | null; error: string | null };
type SuccessResponse<T> = {
data: NonNullable<T>;
error: null;
};
type ErrorResponse = {
data: null;
error: NonNullable<string>;
};
// Union of both
type Result<T> = SuccessResponse<T> | ErrorResponse;
function isSuccess<T>(res: Result<T>): res is SuccessResponse<T> {
return res.data !== null;
}// ReturnType — extract the return type of a function
function createUser(name: string) {
return { name, id: Math.random(), createdAt: new Date() };
}
type User = ReturnType<typeof createUser>;
// { name: string; id: number; createdAt: Date }
// Parameters — extract function parameters as a tuple
function save(user: { name: string }, persist: boolean): void {}
type SaveParams = Parameters<typeof save>;
// [{ name: string }, boolean]
// ConstructorParameters — extract constructor parameters
class UserService {
constructor(private db: string, private timeout: number) {}
}
type CtorParams = ConstructorParameters<typeof UserService>;
// [string, number]
// InstanceType — extract the instance type of a constructor
type ServiceInstance = InstanceType<typeof UserService>;
// UserService
// Awaited — unwrap Promise types (TS 4.5+)
type T6 = Awaited<Promise<string>>; // string
type T7 = Awaited<Promise<Promise<number>>>; // number
type T8 = Awaited<boolean | Promise<number>>; // number | boolean
// NoInfer — prevent TypeScript from inferring a type (TS 5.4+)
function createRoute<T>(path: string, handler: T, options: NoInfer<T>): T {
return handler;
}// Custom utility types — build your own
// Make specific keys required
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Make specific keys optional
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Deep Partial — recursive Partial
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
// Prettify — expand a type for readability (TS 5.4+)
type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
// Exact type — prevent extra properties
type Exact<T, Shape> = T extends Shape
? Exclude<keyof T, keyof Shape> extends never
? T
: never
: never;
// Builder pattern type
type Builder<T> = {
[K in keyof T]?: T[K] extends object
? Builder<T[K]>
: T[K];
} & { build(): T };
// Branded / Opaque type utility
type Brand<T, B> = T & { readonly __brand: B };
type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;
const dollars = 100 as USD;
const euros = 100 as EUR;
// dollars + euros; // Error: cannot mix branded types| Utility | Signature | Result |
|---|---|---|
| Partial<T> | All props optional | { name?: string } |
| Required<T> | All props required | { name: string } |
| Readonly<T> | All props readonly | { readonly name: string } |
| Record<K,V> | Key-value map | { [key: K]: V } |
| Pick<T,K> | Select subset | Only specified keys |
| Omit<T,K> | Exclude keys | All except specified |
| Exclude<U,E> | Remove from union | Filters union members |
| Extract<U,E> | Match from union | Selects union members |
| NonNullable<T> | Remove null/undef | Never null/undefined |
| ReturnType<F> | Function return type | Inferred return |
| Parameters<F> | Function param tuple | [arg1, arg2, ...] |
| Awaited<T> | Unwrap Promise | Nested promise unwrapping |
Omit<Partial<User>, "id"> creates a type with optional fields except id. Combine with Record for flexible data structures.// Conditional types — type-level if/else
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // false
// Nested conditionals
type UnwrapPromise<T> = T extends Promise<infer U>
? U extends Promise<unknown>
? UnwrapPromise<U>
: U
: T;
type C = UnwrapPromise<Promise<Promise<string>>>; // string
// Distributive conditional types
// When T is a union, the conditional is applied to each member
type ToArray<T> = T extends any ? T[] : never;
type D = ToArray<string | number>; // string[] | number[]
// NOT (string | number)[] — it distributes!
// Prevent distribution with [T]
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type E = ToArrayNonDist<string | number>; // (string | number)[]// infer — extract types from within other types
// Works inside conditional types (extends clause)
// Extract return type
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type F = ReturnOf<() => string>; // string
type G = ReturnOf<(x: number) => boolean>; // boolean
// Extract first parameter
type FirstParam<T> = T extends (first: any, ...args: any[]) => any
? T extends (first: infer F, ...args: any[]) => any
? F
: never
: never;
type H = FirstParam<(name: string, age: number) => void>; // string
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type I = ElementOf<string[]>; // string
type J = ElementOf<readonly number[]>; // number
// Extract Promise resolved type
type Resolved<T> = T extends Promise<infer U> ? U : T;
// Extract from template literals (TS 4.8+)
type ParseRoute<T> = T extends `${infer Path}/${infer Param}`
? { path: Path; param: Param }
: never;
type K = ParseRoute<"api/users/:id">; // { path: "api/users"; param: ":id" }// Mapped types — transform every property of a type
type Readonly<T> = { readonly [P in keyof T]: T[P] };
// Remap keys with 'as'
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// Filter keys with 'as' + never
type RemoveMethods<T> = {
[P in keyof T as T[P] extends Function ? never : P]: T[P];
};
// Add modifiers
type OptionalMethods<T> = {
[P in keyof T]?: T[P] extends (...args: any[]) => any ? T[P] : never;
};
// Key remapping with template literals
type EventMap<T> = {
[P in keyof T as `on${Capitalize<string & P>}`]: (
handler: (payload: T[P]) => void
) => void;
};
type UserEvents = EventMap<{
login: { userId: number };
logout: void;
}>;
// { onLogin: (handler) => void; onLogout: (handler) => void }// Template literal types — string manipulation at type level
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// String manipulation types
type Lowercase<S extends string> = S extends `${infer C}${infer Rest}`
? `${Lowercase<string & C>}${Lowercase<Rest>}`
: S;
// Built-in string types
type Uppercased = Uppercase<"hello">; // "HELLO"
type Lowercased = Lowercase<"HELLO">; // "hello"
type Capitalized = Capitalize<"hello">; // "Hello"
type Uncapitalized = Uncapitalize<"Hello">; // "hello"
// Combine with generics
type CSSProperty = `--${string}`;
const valid: CSSProperty = "--primary-color";
// const invalid: CSSProperty = "primary-color"; // Error
// Parse strings into types
type ParseColor<T extends string> =
T extends `rgb(${infer R}, ${infer G}, ${infer B})`
? { r: R; g: G; b: B }
: never;
type Color = ParseColor<"rgb(255, 0, 0)">;
// { r: "255"; g: "0"; b: "0" }// Type narrowing — narrowing the type within a scope
// typeof narrowing
function double(value: string | number) {
if (typeof value === "string") {
return value.repeat(2); // value: string
}
return value * 2; // value: number
}
// instanceof narrowing
class Dog { bark() { console.log("woof"); } }
class Cat { meow() { console.log("meow"); } }
function speak(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // pet: Dog
} else {
pet.meow(); // pet: Cat
}
}
// 'in' operator narrowing
interface Admin { role: string; permissions: string[]; }
interface Guest { role: string; visitCount: number; }
function check(user: Admin | Guest) {
if ("permissions" in user) {
console.log(user.permissions); // user: Admin
} else {
console.log(user.visitCount); // user: Guest
}
}
// Discriminant union narrowing
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rect": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
}
}// Const type parameters (TypeScript 5.0+)
function createRoutes<const T extends readonly string[]>(routes: T) {
return routes;
}
// Previously inferred as string[] — now preserves literal types
const routes = createRoutes(["home", "about", "contact"]);
// routes: readonly ["home", "about", "contact"]
// Works with objects too
function defineConfig<const T extends Record<string, string>>(config: T) {
return config;
}
const config = defineConfig({
api: "https://api.example.com",
port: "3000",
});
// config.api: "https://api.example.com" (literal, not just string)
// satisfies operator (TypeScript 4.9+)
// Validates type without widening
type Color = "red" | "green" | "blue";
// Before: 'const' widens to string
const colors1 = { primary: "red", secondary: "blue" };
// primary: string, secondary: string
// With satisfies: validates AND preserves literals
const colors2 = { primary: "red", secondary: "blue" } satisfies Record<string, Color>;
// primary: "red", secondary: "blue"
// Practical: catches errors while preserving precise types
type Theme = {
colors: Record<string, [number, number, number]>;
spacing: Record<string, number>;
};
const theme = {
colors: {
primary: [255, 0, 0],
secondary: [0, 0, 255],
},
spacing: {
sm: 4,
md: 8,
lg: 16,
},
} satisfies Theme;
// theme.colors.primary: [number, number, number] — exact tuple preserved| Technique | Syntax | Narrows To |
|---|---|---|
| typeof | typeof x === "string" | string / number / boolean / etc. |
| instanceof | x instanceof Dog | Dog class type |
| in | "prop" in obj | Type containing that property |
| Discriminant | obj.kind === "circle" | Matching union member |
| truthiness | if (obj) | Excludes null/undefined/0/"" |
| Equality | x === null | Exact match type |
| Type predicate | x is Dog | Custom assertion function |
never with discriminant unions to ensure all cases are handled — assign to const _exhaustive: never in the default branch. If a new union member is added, TS catches the type error at compile time.// Enable decorators in tsconfig.json:
// "experimentalDecorators": true (legacy)
// OR use TC39 standard (TS 5.0+): no flag needed
// ── Class Decorator (TC39 standard — TS 5.0+) ──
function sealed<T extends { new (...args: any[]): {} }>(target: T) {
return class extends target {
constructor(...args: any[]) {
super(...args);
Object.seal(this);
}
};
}
@sealed
class Report {
title: string;
constructor(title: string) {
this.title = title;
}
}
// ── Decorator factory ──
function addMetadata(metadata: Record<string, string>) {
return <T extends { new (...args: any[]): {} }>(target: T) => {
return class extends target {
_metadata = metadata;
};
};
}
@addMetadata({ version: "1.0", author: "team" })
class ApiService {}// ── Method Decorator ──
function log<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`[${name}] called with:`, args);
const result = target.call(this, ...args);
console.log(`[${name}] returned:`, result);
return result;
};
}
// ── Debounce decorator factory ──
function debounce(ms: number) {
return <This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) => {
let timer: ReturnType<typeof setTimeout>;
return function (this: This, ...args: Args): Return {
clearTimeout(timer);
timer = setTimeout(() => target.call(this, ...args), ms);
return undefined as unknown as Return;
};
};
}
class SearchService {
@log
search(query: string): string[] {
return [query]; // imagine actual search
}
@debounce(300)
suggest(term: string): void {
console.log("Fetching suggestions for:", term);
}
}// ── Property / Accessor Decorator ──
function enumerable(value: boolean) {
return <This, Value>(
target: ClassAccessorDecoratorTarget<This, Value>,
context: ClassAccessorDecoratorContext<This, Value>
): ClassAccessorDecoratorResult<This, Value> => {
return {
get() {
return target.get.call(this);
},
set(value: Value) {
target.set.call(this, value);
},
};
};
}
// ── Parameter Decorator (use with reflect-metadata) ──
import "reflect-metadata";
function required(target: any, methodName: string, paramIndex: number) {
const existing: number[] =
Reflect.getOwnMetadata("required", target, methodName) || [];
existing.push(paramIndex);
Reflect.defineMetadata("required", existing, target, methodName);
}
class FormValidator {
validate(@required name: string, @required email: string) {
const requiredParams: number[] =
Reflect.getOwnMetadata("required", this, "validate") || [];
const args = [name, email];
for (const idx of requiredParams) {
if (!args[idx]) {
throw new Error(`Parameter at index ${idx} is required`);
}
}
}
}// ── TypeScript 5.0+ Standard Decorators ──
// New TC39 stage 3 proposal — better typing, no legacy quirks
// Accessor decorator — wraps get/set with custom logic
function capitalize(target: ClassAccessorDecoratorTarget<any, string>) {
return {
set(value: string) {
target.set.call(this, value.charAt(0).toUpperCase() + value.slice(1));
},
get() {
return target.get.call(this);
},
};
}
class UserProfile {
@capitalize accessor firstName: string = "";
@capitalize accessor lastName: string = "";
}
const profile = new UserProfile();
profile.firstName = "alice"; // stored as "Alice"
console.log(profile.firstName); // "Alice"
// ── Legacy vs Standard comparison ──
// Legacy (experimentalDecorators):
// - Decorator receives target + propertyKey + descriptor
// - Mutates descriptor directly
// - Context object is separate arguments
// Standard (TS 5.0+, no flag):
// - Decorator receives (target, context)
// - Returns replacement value
// - Context is a single object with name, kind, access, etc.
// - Better type inference with generics
// - Works with accessors (get/set pairs)| Decorator | Kind | Applies To |
|---|---|---|
| Class | class | Class declarations |
| Method | method | Class methods |
| Accessor | accessor | get/set (auto-accessor) |
| Property | field | Class fields |
| Parameter | parameter | Function parameters |
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false,
"emitDecoratorMetadata": false
}
}experimentalDecorators mode. They offer better type safety, proper this context, and align with the TC39 ECMAScript proposal. Migrate when targeting ES2022+.// ── Type-safe Builder Pattern ──
interface QueryConfig {
table: string;
select: string[];
where: string[];
orderBy: string;
limit: number;
}
type BuilderStep<K extends keyof QueryConfig> = {
[P in K]: (value: QueryConfig[P]) => BuilderStep<Exclude<keyof QueryConfig, P>>;
} & {
build: () => QueryConfig;
};
function createQuery(): BuilderStep<never> {
const config = {} as Partial<QueryConfig>;
const handler: ProxyHandler<BuilderStep<never>> = {
get(_, prop) {
if (prop === "build") return () => config as QueryConfig;
return (value: unknown) => {
config[prop as keyof QueryConfig] = value as never;
return handler; // return proxy for chaining
};
},
};
return new Proxy({} as BuilderStep<never>, handler);
}
// Usage:
const query = createQuery()
.table("users")
.select(["name", "email"])
.where(["active = true"])
.orderBy("name")
.limit(10)
.build();
// { table: "users", select: [...], where: [...], orderBy: "name", limit: 10 }// ── Type-Safe Event Emitter ──
type EventMap = {
login: { userId: number; timestamp: Date };
logout: { userId: number };
error: { message: string; code: number };
data: { payload: unknown };
};
class TypedEmitter<Events extends Record<string, any>> {
private listeners = new Map<keyof Events, Set<Function>>();
on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
off<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
this.listeners.get(event)?.delete(listener);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach((fn) => fn(data));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("login", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.on("error", ({ message, code }) => {
console.error(`Error ${code}: ${message}`);
});
emitter.emit("login", { userId: 1, timestamp: new Date() });
// emitter.emit("login", { userId: 1 }); // Error: missing timestamp
// emitter.emit("unknown", {}); // Error: "unknown" not in EventMap// ── Branded Types (Nominal Typing) ──
// Prevent accidental mixing of compatible types
type Brand<T, B extends string> = T & { readonly __brand: B };
type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;
type UserId = Brand<number, "UserId">;
type OrderId = Brand<number, "OrderId">;
function createUSD(amount: number): USD {
return amount as USD;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const wallet = createUSD(100);
const tax = createUSD(20);
const total = addUSD(wallet, tax); // USD
// addUSD(wallet, 50 as EUR); // Error: EUR is not USD
// ── Opaque Types (Module-level branding) ──
// types.ts
export type UserId = number & { readonly __userId: unique symbol };
export type OrderId = number & { readonly __orderId: unique symbol };
export function createUserId(id: number): UserId {
return id as UserId;
}
export function createOrderId(id: number): OrderId {
return id as OrderId;
}
// Usage
import { UserId, OrderId, createUserId } from "./types";
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = createUserId(42);
getUser(userId); // OK
// getOrder(userId); // Error: UserId is not OrderId
// getUser(42); // Error: number is not UserId// ── Recommended strict tsconfig.json ──
{
"compilerOptions": {
// Language & Environment
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
// Strict Type Checking
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
// Code Quality
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
// Module Settings
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
// Output
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}// ── Module Augmentation ──
// Extend third-party types without modifying source
// Augment the Express Request object
declare namespace Express {
interface Request {
userId?: number;
userRole?: "admin" | "editor" | "viewer";
}
}
// Augment the global Window interface
declare global {
interface Window {
analytics: {
track: (event: string, data?: Record<string, unknown>) => void;
identify: (userId: string, traits?: Record<string, unknown>) => void;
};
env: Record<string, string>;
}
}
// Now use globally
window.analytics?.track("page_view", { url: "/home" });
// ── Declaration Files (.d.ts) ──
// types/api.d.ts — describe external JS modules
declare module "my-sdk" {
export interface ClientOptions {
apiKey: string;
baseUrl?: string;
timeout?: number;
}
export class Client {
constructor(options: ClientOptions);
request<T>(endpoint: string, data?: unknown): Promise<T>;
disconnect(): void;
}
}
// Use the declared module
import { Client } from "my-sdk";
const client = new Client({ apiKey: "secret" });
client.request("/users");noUncheckedIndexedAccess — it makes arr[0] return T | undefined instead of just T, catching out-of-bounds bugs at compile time. Combined with strict mode, this is the gold standard for type safety.interface supports extends and implements for classes, allows declaration merging (auto-combines same-name declarations), and produces better error messages. type supports unions (A | B), intersections (A & B), mapped types, conditional types, template literals, and tuples. Use interface for object shapes that need extending; use type for computed and union types.
// Interface: extends + declaration merging
interface Animal { name: string; }
interface Animal { age: number; } // merged!
class Dog implements Animal { name = ""; age = 0; }
// Type: unions, conditionals, mapped types
type Status = "open" | "closed";
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Unwrap<T> = T extends Array<infer U> ? U : T;any completely disables type checking — you can assign it to any type and perform any operation on it. unknown is type-safe — you cannot perform operations on it without first narrowing the type. Always prefer unknown.
let a: any = "hello";
a.foo(); // No error — dangerous!
let b: unknown = "hello";
// b.foo(); // Error: Object is of type 'unknown'
if (typeof b === "string") {
b.toUpperCase(); // OK after narrowing
}Generics let you write reusable, type-safe code that works with any type while maintaining compile-time type information. Use them for functions, classes, and interfaces that need to operate on multiple types without losing type safety.
// Generic function with constraint
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
getFirst([1, 2, 3]); // T = number
getFirst(["a", "b"]); // T = string
// Constrained generic
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // string
// getProperty(user, "email"); // ErrorThe satisfies operator (TS 4.9+) validates that a value matches a type without widening it. It catches type errors while preserving the most specific literal types for further use.
type Theme = {
colors: Record<string, [number, number, number]>;
};
// Without satisfies: colors inferred as Record<string, number[]>
// With satisfies: colors inferred as { primary: [number,number,number]; ... }
const theme = {
colors: {
primary: [255, 0, 0],
secondary: [0, 0, 255],
},
} satisfies Theme;
// You get both: validation AND precise types
const [r, g, b] = theme.colors.primary; // Works! tuple preservedConditional types (T extends U ? X : Y) perform type-level branching. The infer keyword extracts types from within other types — it declares a type variable that TypeScript infers from the matching position.
// Conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// infer extracts types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type C = ReturnType<() => string>; // string
type ElementType<T> = T extends (infer E)[] ? E : never;
type D = ElementType<string[]>; // string
// Nested infer
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type E = Awaited<Promise<Promise<number>>>; // numberMapped types iterate over the keys of a type to produce a new type. They are the foundation of most built-in utility types like Partial, Readonly, and Record. With key remapping (as), you can filter and transform keys.
// Basic mapped type
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };
// Key remapping (TS 4.1+)
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Config { host: string; port: number; }
type ConfigGetters = Getters<Config>;
// { getHost: () => string; getPort: () => number }
// Filter keys with 'as' + never
type NoMethods<T> = {
[P in keyof T as T[P] extends Function ? never : P]: T[P];
};Type narrowing is the process of TypeScript reducing a broad type to a more specific type within a conditional branch. This enables type-safe access to properties and methods.
// 1. typeof
function handle(value: string | number) {
if (typeof value === "string") return value.toUpperCase();
return value.toFixed(2);
}
// 2. Discriminant union
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
function unwrap<T>(res: Result<T>): T {
if (res.ok) return res.value; // TS knows res is { ok: true; value: T }
throw new Error(res.error); // TS knows res is { ok: false; error: string }
}
// 3. Type predicates
function isString(val: unknown): val is string {
return typeof val === "string";
}
const items: unknown[] = [1, "hello", null];
const strings = items.filter(isString); // string[] (not unknown[])
// 4. 'in' operator
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(pet: Fish | Bird) {
if ("swim" in pet) pet.swim();
else pet.fly();
}Const type parameters (TS 5.0+) use <const T> to infer literal types instead of widened types. This preserves precise string/number/boolean literals in function arguments, making downstream type inference more accurate.
// Without const: inferred as string[]
function createRoutes<T extends readonly string[]>(routes: T) {
return routes;
}
const routes1 = createRoutes(["home", "about"]);
// Without <const>: string[] (widened)
// With <const>: readonly ["home", "about"] (literal types preserved)
// The key difference
function defineConfig<const T extends Record<string, string>>(c: T) {
return c;
}
const config = defineConfig({
theme: "dark",
lang: "en",
});
// config.theme: "dark" — literal type preserved
// Can be used in switch/case, satisfies, etc.Declaration files describe the shape of JavaScript code to TypeScript without emitting any runtime code. Module augmentation allows you to extend existing types (including third-party library types) without modifying the original source.
// Declaration file — describe external module
// my-lib.d.ts
declare module "my-lib" {
export interface Config { debug: boolean; port: number; }
export function createApp(config: Config): App;
export class App { start(): void; stop(): void; }
}
// Module augmentation — extend existing types
// global.d.ts
declare global {
interface Window {
myApp: { version: string; build: number };
}
interface Array<T> {
myCustomMethod(): T[];
}
}
// Now available everywhere
window.myApp.version; // string
[1, 2, 3].myCustomMethod();Regular enums generate runtime JavaScript objects. Const enums are inlined at compile time (no runtime code). However, many teams prefer union types of string literals as a modern alternative — they provide similar type safety without any runtime overhead, work better with tree-shaking, and are more idiomatic TypeScript.
// Regular enum — generates runtime code
enum Direction { Up, Down, Left, Right }
// JS output: var Direction; (function(Direction) { ... })(Direction || (Direction = {}));
// Const enum — inlined, no runtime
const enum Perm { Read, Write, Admin }
const p = Perm.Read; // JS output: var p = 0 /* Read */;
// Modern alternative: union of literal types
type Direction = "up" | "down" | "left" | "right";
function move(dir: Direction) { /* ... */ }
move("up"); // OK
// move("diag"); // Error
// Object-as-enum pattern (runtime values + type safety)
const Status = {
Active: "active",
Inactive: "inactive",
Pending: "pending",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// Status: "active" | "inactive" | "pending"
// typeof Status: { readonly Active: "active"; readonly Inactive: "inactive"; ... }