DevToolBoxZA DARMO
Blog

Advanced TypeScript Guide: Generics, Conditional Types, Mapped Types, Decorators, and Type Narrowing

18 min readby DevToolBox

Advanced TypeScript Guide: Generics, Conditional Types, Template Literals, Decorators & Type-Level Programming

A comprehensive deep-dive into TypeScript's most powerful type system features, from advanced generics to type-level programming patterns.

TL;DRTypeScript's type system is Turing-complete, enabling expressive compile-time guarantees far beyond basic annotations. This guide covers advanced generics (constraints, defaults, variance), conditional types with infer, mapped types and key remapping, template literal types, utility types deep dive, discriminated unions with exhaustive checks, branded/nominal types, declaration merging, module augmentation, Stage 3 decorators, the satisfies operator, const assertions, type narrowing patterns, covariance/contravariance, and recursive types.
Key Takeaways
  • Generic constraints and defaults make reusable APIs both flexible and type-safe.
  • Conditional types with infer enable powerful type extraction and transformation.
  • Mapped types with key remapping and template literals create derived types automatically.
  • Discriminated unions plus exhaustive checks eliminate entire categories of runtime bugs.
  • Branded types provide nominal typing in a structural type system.
  • The satisfies operator validates types without widening, preserving literal inference.

Why Advanced TypeScript Matters

Basic TypeScript annotations (string, number, boolean) only scratch the surface. The type system includes generics, conditional types, mapped types, template literal types, and recursive types that let you express complex domain rules at compile time.

Mastering these features reduces runtime errors, improves API design, and enables patterns like type-safe event systems, validated API responses, and fully typed ORM queries. This guide walks through each feature with practical examples.

1. Advanced Generics

Generics are the foundation of reusable typed code. Beyond basic usage, TypeScript supports generic constraints, default type parameters, and variance annotations that give you fine-grained control.

Generic Constraints

Generic constraints use the extends keyword to limit what types a generic parameter can accept. This ensures the generic works only with types that have the required shape.

// Generic constraint: T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");        // OK: string has length
longest([1, 2, 3], [1]);       // OK: array has length
// longest(10, 20);            // Error: number has no length

// Constraint with keyof
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");     // type: string
getProperty(user, "age");      // type: number
// getProperty(user, "email"); // Error: "email" not in keyof User

Default Type Parameters

Default type parameters provide fallback types when the caller does not specify one. This is analogous to default function parameters and reduces boilerplate at call sites.

// Default type parameter
interface ApiResponse<TData = unknown, TError = Error> {
  data: TData | null;
  error: TError | null;
  status: number;
}

// Uses defaults: ApiResponse<unknown, Error>
const res1: ApiResponse = { data: null, error: null, status: 200 };

// Override only TData
const res2: ApiResponse<User[]> = {
  data: [{ id: 1, name: "Alice" }],
  error: null,
  status: 200,
};

Variance Annotations

Variance annotations (in and out keywords, added in TypeScript 4.7) explicitly mark whether a generic parameter is covariant (out), contravariant (in), or invariant. This improves type checking correctness and performance.

// Variance annotations (TypeScript 4.7+)
interface Producer<out T> {
  produce(): T;    // T is in output (covariant) position
}

interface Consumer<in T> {
  consume(value: T): void;  // T is in input (contravariant) position
}

interface Transform<in T, out U> {
  transform(input: T): U;   // T contravariant, U covariant
}

2. Conditional Types & Infer

Conditional types follow the pattern T extends U ? X : Y. They enable type-level branching, choosing different types based on whether a condition is met.

// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;   // true
type B = IsString<42>;        // false
type C = IsString<string>;    // true

// Nested conditional types
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

The infer Keyword

The infer keyword inside conditional types introduces a type variable that TypeScript infers from the checked type. This is how utility types like ReturnType and Parameters work.

// Extract return type using infer
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;          // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// Extract Promise inner type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type P1 = UnwrapPromise<Promise<string>>;      // string
type P2 = UnwrapPromise<Promise<number[]>>;    // number[]
type P3 = UnwrapPromise<string>;               // string

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;

type E1 = ElementOf<string[]>;    // string
type E2 = ElementOf<[1, 2, 3]>;   // 1 | 2 | 3

Distributive Conditional Types

Distributive conditional types automatically distribute over union types. If T is A | B, then T extends U ? X : Y becomes (A extends U ? X : Y) | (B extends U ? X : Y). Wrap T in [T] to prevent distribution.

// Distributive: T distributes over union
type ToArray<T> = T extends any ? T[] : never;

type D1 = ToArray<string | number>;
// Result: string[] | number[]  (distributed)

// Non-distributive: wrap in tuple
type ToArrayND<T> = [T] extends [any] ? T[] : never;

type D2 = ToArrayND<string | number>;
// Result: (string | number)[]  (not distributed)

3. Mapped Types & Key Remapping

Mapped types iterate over the keys of a type to create new types. The basic form is { [K in keyof T]: NewType }. Combined with conditional types and template literals, mapped types become extremely powerful.

// Basic mapped type: make all properties optional
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Mapped type with conditional value
type NullableProps<T> = {
  [K in keyof T]: T[K] | null;
};

Key Remapping (as clause)

Key remapping (as clause, TypeScript 4.1+) lets you transform keys during mapping. You can rename, filter, or generate new keys using template literal types.

// Key remapping with as (TypeScript 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// Filter keys: remove methods
type DataOnly<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

Template Literal Types

Template literal types (introduced in TypeScript 4.1) enable string manipulation at the type level. Combined with mapped types, they can generate typed event handlers, API endpoints, or CSS property types.

// Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<"click">;     // "onClick"
type FocusEvent = EventName<"focus">;     // "onFocus"

// Combining with union types
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";

type ColorSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large"
// | "blue-small" | "blue-medium" | ...

// Built-in string manipulation types
type U = Uppercase<"hello">;       // "HELLO"
type L = Lowercase<"HELLO">;       // "hello"
type Cap = Capitalize<"hello">;    // "Hello"
type Unc = Uncapitalize<"Hello">;  // "hello"

4. Utility Types Deep Dive

TypeScript ships with built-in utility types that are implemented using generics, conditional types, and mapped types. Understanding their internals helps you write your own.

Tip: Partial<T> makes all properties optional. Required<T> makes all required. Pick<T, K> selects specific keys. Omit<T, K> removes keys. Record<K, V> creates object types. These are the most commonly used.
Utility TypeImplementationPurpose
Partial<T>{ [K in keyof T]?: T[K] }All properties optional
Required<T>{ [K in keyof T]-?: T[K] }All properties required
Pick<T, K>{ [P in K]: T[P] }Select specific keys
Omit<T, K>Pick<T, Exclude<keyof T, K>>Remove specific keys
Record<K, V>{ [P in K]: V }Object with key type K, value type V
ReturnType<T>T extends (...) => infer R ? R : anyExtract function return type
Parameters<T>T extends (...args: infer P) => any ? P : neverExtract parameter types as tuple
Awaited<T>Recursive Promise unwrapUnwrap nested Promises

Exclude<T, U> removes types from a union. Extract<T, U> keeps matching types. ReturnType<T> extracts the return type of a function. Parameters<T> extracts parameter types as a tuple. NonNullable<T> removes null and undefined.

// Practical utility type compositions
type UpdatePayload<T> = Partial<Omit<T, "id" | "createdAt">>;

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

type UserUpdate = UpdatePayload<User>;
// { name?: string; email?: string }

// Custom utility: make specific keys required
type RequireKeys<T, K extends keyof T> =
  Omit<T, K> & Required<Pick<T, K>>;

type UserWithEmail = RequireKeys<Partial<User>, "email">;
// { id?: string; name?: string; email: string; createdAt?: Date }

Awaited<T> (TypeScript 4.5+) unwraps Promise types recursively. NoInfer<T> (TypeScript 5.4+) prevents inference at specific positions. These newer utilities solve common pain points.

5. Discriminated Unions & Exhaustive Checks

Discriminated unions combine union types with a shared literal property (the discriminant). TypeScript narrows the type based on this discriminant, enabling safe access to variant-specific properties.

// Discriminated union with "type" discriminant
type Shape =
  | { type: "circle"; radius: number }
  | { type: "rectangle"; width: number; height: number }
  | { type: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.type) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

Exhaustive Checking with never

Exhaustive checking ensures every variant is handled. The never type is key: if a switch statement misses a case, the remaining type is not assignable to never, causing a compile error.

// Exhaustive check helper
function assertNever(x: never): never {
  throw new Error("Unexpected value: " + x);
}

function getShapeColor(shape: Shape): string {
  switch (shape.type) {
    case "circle":    return "red";
    case "rectangle": return "blue";
    case "triangle":  return "green";
    default:
      // If a new variant is added to Shape but not handled,
      // TypeScript will error here at compile time
      return assertNever(shape);
  }
}

This pattern is ideal for state machines, Redux actions, API responses with different shapes, compiler AST nodes, and any scenario with multiple variants sharing a common interface.

Tip: Use discriminated unions for Redux actions, API responses, AST nodes, and any domain model with multiple variants. The compiler guarantees every variant is handled.

6. Branded & Nominal Types

TypeScript uses structural typing: two types with the same shape are compatible. Branded types add a phantom property to create nominal-like types that are structurally incompatible even if their runtime values are identical.

// Branded type pattern
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;

// Constructor functions with validation
function createUserId(id: string): UserId {
  if (!id.startsWith("usr_")) {
    throw new Error("Invalid user ID format");
  }
  return id as UserId;
}

function createEmail(value: string): Email {
  if (!value.includes("@")) {
    throw new Error("Invalid email");
  }
  return value as Email;
}

function getUser(id: UserId): void { /* ... */ }
function getOrder(id: OrderId): void { /* ... */ }

const userId = createUserId("usr_123");
const orderId = "ord_456" as OrderId;

getUser(userId);   // OK
// getUser(orderId); // Error: OrderId not assignable to UserId

This is critical for preventing accidental misuse: passing a UserId where an OrderId is expected, or mixing validated and unvalidated strings. The brand exists only at the type level with zero runtime cost.

Warning: The brand property is purely a compile-time construct. At runtime, branded values are just regular strings/numbers. Never access the __brand property directly.

7. Declaration Merging & Module Augmentation

Declaration merging automatically combines multiple declarations of the same name. Interfaces merge their members. Namespaces merge with classes, functions, and enums. This is how TypeScript extends built-in types.

// Interface merging
interface Window {
  analytics: {
    track(event: string, data?: object): void;
  };
}

// Now window.analytics.track() is typed
window.analytics.track("page_view", { path: "/" });

Module Augmentation

Module augmentation lets you extend third-party library types without modifying their source. Use declare module to add properties to Express Request, extend Window, or patch library types.

// Augment Express Request type
declare module "express" {
  interface Request {
    user?: {
      id: string;
      role: "admin" | "user";
    };
    requestId: string;
  }
}

// Augment a CSS module
declare module "*.module.css" {
  const classes: Record<string, string>;
  export default classes;
}

// Augment environment variables
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    API_KEY: string;
    NODE_ENV: "development" | "production" | "test";
  }
}

8. Decorators (Stage 3 / TC39)

Stage 3 decorators (TypeScript 5.0+) are a standard proposal for modifying classes, methods, properties, and accessors at definition time. They replace the older experimental decorators that required the --experimentalDecorators flag.

A decorator is a function that receives a target value and a context object. It can return a replacement value or undefined. Class decorators receive the class constructor. Method decorators receive the method function.

// Stage 3 decorator: method logging
function log<T extends (...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext
): T {
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with`, args);
    const result = target.apply(this, args);
    console.log(`${methodName} returned`, result);
    return result;
  } as T;
}

// Class decorator: sealed
function sealed<T extends new (...args: any[]) => any>(
  target: T,
  _context: ClassDecoratorContext
): T {
  Object.seal(target);
  Object.seal(target.prototype);
  return target;
}
// Using decorators
@sealed
class UserService {
  @log
  findById(id: string): User | null {
    // ... database lookup
    return null;
  }

  @log
  create(data: CreateUserDto): User {
    // ... insert logic
    return { id: "1", ...data } as User;
  }
}

Common use cases include logging, validation, memoization, dependency injection, access control, and serialization metadata. Decorators compose naturally when stacked.

9. The satisfies Operator

The satisfies operator (TypeScript 4.9+) validates that an expression matches a type without changing the inferred type. Unlike type annotations (: Type), satisfies preserves literal types and specific union members.

// Without satisfies: type annotation widens
type ColorMap = Record<string, [number, number, number] | string>;

const colorsAnnotated: ColorMap = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
};
// colorsAnnotated.red is [number, number, number] | string
// Cannot call .toUpperCase() even on green!

// With satisfies: preserves literal inference
const colors = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies ColorMap;

colors.green.toUpperCase();  // OK! TypeScript knows it is string
colors.red[0];              // OK! TypeScript knows it is number

This solves a long-standing tension: you want type checking for correctness but do not want to lose the specific inferred type. With satisfies, you get both.

// Route config with satisfies
type Route = { path: string; component: string; auth?: boolean };

const routes = {
  home:    { path: "/",        component: "HomePage" },
  profile: { path: "/profile", component: "ProfilePage", auth: true },
  login:   { path: "/login",   component: "LoginPage" },
} satisfies Record<string, Route>;

// routes.home.path is "/", not just string!
// routes.profile.auth is true, not just boolean | undefined!

10. Const Assertions & Readonly Patterns

The as const assertion (const assertion) converts a value to its most specific literal type. Arrays become readonly tuples, objects get readonly properties with literal types, and string values are narrowed to their exact literal.

// as const narrows to literal types
const config = {
  endpoint: "https://api.example.com",
  retries: 3,
  methods: ["GET", "POST"],
} as const;

// typeof config:
// {
//   readonly endpoint: "https://api.example.com";
//   readonly retries: 3;
//   readonly methods: readonly ["GET", "POST"];
// }

// Derive union type from const array
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE"] as const;
type HttpMethod = (typeof HTTP_METHODS)[number];
// "GET" | "POST" | "PUT" | "DELETE"

Combining as const with generic functions enables powerful patterns like type-safe builders, route definitions, and configuration objects where the exact shape is preserved at the type level.

// Type-safe route builder with as const
function defineRoutes<
  T extends Record<string, { path: string }>
>(routes: T): T {
  return routes;
}

const appRoutes = defineRoutes({
  home:    { path: "/" },
  about:   { path: "/about" },
  blog:    { path: "/blog" },
} as const);

// appRoutes.home.path is exactly "/", not string

11. Type Narrowing Patterns

Type narrowing is how TypeScript refines a broad type to a more specific one within a code block. Beyond typeof and instanceof, TypeScript supports custom type guards, assertion functions, and control flow narrowing.

User-Defined Type Guards

User-defined type guards (is keyword) return a boolean and narrow the parameter type. Assertion functions (asserts keyword) throw if the condition fails and narrow the type after the call.

// Type guard with is keyword
interface Fish { swim(): void }
interface Bird { fly(): void }

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();  // TypeScript knows pet is Fish
  } else {
    pet.fly();   // TypeScript knows pet is Bird
  }
}

// Assertion function (asserts keyword)
function assertDefined<T>(
  value: T | null | undefined,
  message?: string
): asserts value is T {
  if (value == null) {
    throw new Error(message ?? "Value is null or undefined");
  }
}

function processUser(user: User | null) {
  assertDefined(user, "User not found");
  // After this line, user is narrowed to User
  console.log(user.name);
}

Advanced Narrowing Patterns

The in operator, equality checks, truthiness checks, and assignment narrowing all refine types. TypeScript tracks narrowing through if/else branches, switch statements, and short-circuit evaluation.

// in operator narrowing
type Admin = { role: "admin"; permissions: string[] };
type Guest = { role: "guest"; visitCount: number };

function greet(user: Admin | Guest) {
  if ("permissions" in user) {
    // user is Admin
    console.log("Admin with", user.permissions.length, "perms");
  } else {
    // user is Guest
    console.log("Guest visit #", user.visitCount);
  }
}

12. Covariance & Contravariance

Covariance means a subtype can substitute for a supertype (Cat[] assignable to Animal[]). Contravariance means the opposite direction: a supertype can substitute for a subtype. This matters for function parameters and return types.

// Covariance: subtype in output position
class Animal { name = "animal"; }
class Dog extends Animal { breed = "labrador"; }

// Return type is covariant:
// () => Dog is assignable to () => Animal
type AnimalFactory = () => Animal;
const dogFactory: AnimalFactory = (): Dog => new Dog();

// Contravariance: supertype in input position
// (with --strictFunctionTypes)
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;

// AnimalHandler is NOT assignable to DogHandler
// (contravariant: parameter types go in opposite direction)
// const handler: DogHandler = (a: Animal) => {}; // Error

Function parameters are contravariant (unless --strictFunctionTypes is off). Return types are covariant. Understanding variance prevents subtle bugs when passing callbacks, comparing function types, or using generic containers.

Watch out: Without --strictFunctionTypes, function parameters are bivariant (both co- and contravariant), which is unsound. Always enable strict mode for correct variance checking.

13. Recursive Types

Recursive types reference themselves in their definition. They are essential for modeling tree structures, nested JSON, deeply nested configs, linked lists, and any data with unbounded depth.

// Recursive type: JSON value
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

// Recursive type: tree structure
type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};

const tree: TreeNode<string> = {
  value: "root",
  children: [
    { value: "child1", children: [] },
    {
      value: "child2",
      children: [
        { value: "grandchild", children: [] }
      ],
    },
  ],
};

TypeScript supports recursive type aliases (since 3.7) and recursive conditional types (since 4.1). These enable powerful utilities like DeepPartial, DeepReadonly, deep path extraction, and JSON type validation.

// DeepPartial: recursively make all properties optional
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? DeepPartial<T[K]>
    : T[K];
};

// DeepReadonly: recursively make all properties readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

// Recursive path extraction
type Path<T, Prefix extends string = ""> = {
  [K in keyof T & string]: T[K] extends object
    ? Path<T[K], `${Prefix}${K}.`>
    : `${Prefix}${K}`;
}[keyof T & string];

interface Config {
  db: { host: string; port: number };
  cache: { ttl: number };
}

type ConfigPaths = Path<Config>;
// "db.host" | "db.port" | "cache.ttl"

Conclusion

TypeScript's advanced type features form a complete type-level programming language. By mastering generics, conditional types, mapped types, template literals, and the patterns in this guide, you can build APIs that are self-documenting, impossible to misuse, and catch errors at compile time rather than runtime.

Start with the patterns most relevant to your current codebase. Discriminated unions and exhaustive checks provide immediate value. Branded types and custom utility types become essential as your domain model grows. Advanced patterns like recursive types and template literal types unlock capabilities that were previously impossible in a statically typed language.

FAQ

When should I use generics vs union types?

Use generics when you need to preserve and propagate a specific type through a function or class. Use union types when you have a known, finite set of possible types. Generics maintain the relationship between input and output types; unions simply allow multiple types at a position.

What is the difference between type and interface in TypeScript?

Interfaces support declaration merging and can be extended with extends. Types support union, intersection, conditional, and mapped types. For object shapes, either works; prefer interface for public APIs (mergeable) and type for complex type operations.

How do conditional types with infer work?

Conditional types use the pattern T extends U ? X : Y. The infer keyword inside the extends clause introduces a type variable that TypeScript infers from T. For example, T extends Promise<infer R> ? R : T extracts the resolved type from a Promise.

What are branded types and when should I use them?

Branded types add a phantom property (existing only at compile time) to create nominal-like types in TypeScript's structural system. Use them to prevent mixing semantically different values like UserId vs OrderId, or validated vs unvalidated strings.

How does the satisfies operator differ from a type annotation?

A type annotation (const x: Type) widens the inferred type to match the annotation. The satisfies operator (const x = value satisfies Type) checks the value against the type but preserves the original narrow/literal inference. You get type checking without losing specificity.

What are Stage 3 decorators and how do they differ from experimental decorators?

Stage 3 decorators (TypeScript 5.0+) follow the TC39 standard proposal and do not require --experimentalDecorators. They receive a value and context object instead of target/key/descriptor. They are not backward-compatible with legacy decorators used by older libraries like Angular or MobX.

How can I make exhaustive checks in a switch statement?

After handling all known cases, add a default case that assigns the discriminant to a variable typed as never. If you miss a case, TypeScript will error because the remaining type is not assignable to never. This ensures all variants are handled at compile time.

What are recursive types used for?

Recursive types model self-referencing data like trees, nested JSON, linked lists, and deeply nested configurations. They enable utility types like DeepPartial, DeepReadonly, and deep path extraction that operate on arbitrarily nested structures.

𝕏 Twitterin LinkedIn
Czy to było pomocne?

Bądź na bieżąco

Otrzymuj cotygodniowe porady i nowe narzędzia.

Bez spamu. Zrezygnuj kiedy chcesz.

Try These Related Tools

{ }JSON Formatter.*Regex TesterTSTypeScript Playground

Related Articles

TypeScript Type Guards: Kompletny Przewodnik Sprawdzania Typów

Opanuj type guards TypeScript: typeof, instanceof, in i niestandardowe guardy.

Kompletny przewodnik po React Hooks: useState, useEffect i Custom Hooks

Opanuj React Hooks z praktycznymi przykladami. useState, useEffect, useContext, useReducer, useMemo, useCallback, custom hooks i React 18+ concurrent hooks.

NestJS Complete Guide: Modules, Controllers, Services, DI, TypeORM, JWT Auth, and Testing

Master NestJS from scratch. Covers modules, controllers, services, providers, dependency injection, TypeORM/Prisma database integration, JWT authentication, Guards, Pipes, Interceptors, Exception Filters, and unit/e2e testing with Jest.