An exhaustive, battle-tested, enterprise-grade curriculum and reference manual for mastering TypeScript 5.x, modern type theory, advanced metaprogramming, compiler architecture, and industrial design patterns.
Stage 1: Tooling • Stage 2: Types & Contracts • Stage 3: Generics & Utilities • Stage 4: Metaprogramming • Stage 5: CFA & Decorators • Stage 6: Architecture • Stage 7: Staff Handbook
flowchart LR
S1["Stage 1: Tooling & Compiler"] --> S2["Stage 2: Types & Contracts"]
S2 --> S3["Stage 3: Generics & Utilities"]
S3 --> S4["Stage 4: Metaprogramming"]
S4 --> S5["Stage 5: CFA & Decorators"]
S5 --> S6["Stage 6: Architecture & Testing"]
S6 --> S7["Stage 7: Staff Architect Handbook"]
| Stage | Focus Domain | Core Concepts & Production Outcomes |
|---|---|---|
| Stage 1 | Absolute Beginner Foundations & Tooling | Type erasure model, structural typing, tsc compiler phases (parser, binder, checker, emitter), strict mode flags, project references. |
| Stage 2 | Core Type Primitives, Objects & Interfaces | Primitives, top/bottom types (any, unknown, never, void), as const, interfaces vs type aliases, excess property checks, modern classes. |
| Stage 3 | Generics & Utility Types Deep Dive | Generic constraints (extends), keyof, TS 5 const type parameters, variance, utility types (Partial, Pick, Omit, Awaited, etc.). |
| Stage 4 | Advanced Type Gymnastics & Metaprogramming | Mapped types with key remapping (as), distributive conditional types, infer type inference, template literals, satisfies, branded types. |
| Stage 5 | Control Flow Analysis, Modules & Decorators | CFA engine, type predicates (is), assertion signatures (asserts), discriminated unions, ambient .d.ts, ESM/CJS, Stage 3 Decorators. |
| Stage 6 | Enterprise Architecture & Design Patterns | Strongly-typed Result/Either, typed EventBus, typed REST client, type-level testing with Vitest (expectTypeOf, assertType). |
| Stage 7 | Staff TypeScript Architect Interview Handbook | 40 staff-level interview scenarios, utility types encyclopedia, essential tsconfig presets, production cheatsheet. |
- 1. Stage 1: Absolute Beginner Foundations & Tooling
- 2. Stage 2: Core Type Primitives, Objects & Interfaces
- 3. Stage 3: Generics & Utility Types Deep Dive
- 4. Stage 4: Advanced Type Gymnastics & Metaprogramming
- 5. Stage 5: Control Flow Analysis, Modules & Decorators
- 6. Stage 6: Enterprise Architecture & Design Patterns
- 7. Stage 7: Staff TypeScript Architect Interview Handbook & Cheatsheet
- 🤝 Community & Contributing
- 📄 Author & Sponsorship
Developed by Anders Hejlsberg (lead architect of C# and Turbo Pascal) at Microsoft, TypeScript is an open-source, strongly typed syntactic superset of JavaScript that compiles down to pristine, standard JavaScript running in any browser, Node.js, Deno, or Bun engine.
┌──────────────────────────────────────────────┐
│ TypeScript │
│ Static Types, Generics, Decorators, Enums │
│ ┌────────────────────────────────────────┐ │
│ │ JavaScript │ │
│ │ Objects, Closures, Promises, Classes │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
- Elimination of Common Runtime Errors: Eliminates the notorious
TypeError: Cannot read properties of undefinedand unintended type coercions at compile time. - Self-Documenting Codebase: Type signatures serve as living, compiler-enforced API contracts that never drift out of sync with actual business logic.
- Fearless Refactoring: Renaming a field or restructuring a function signature updates every reference across millions of lines of code with zero guesswork.
- Developer Tooling Powerhouse: Powers IntelliSense, autocompletion, instant parameter documentation, and jump-to-definition in VS Code, JetBrains IDEs, and modern AI coding agents.
TypeScript’s type system is erased at compile time. The compiler analyzes your source code, emits type errors if any constraints are violated, and then strips away all interfaces, type aliases, type annotations, and type assertions—leaving behind clean, unencumbered JavaScript.
TypeScript Source File (.ts)
│
▼
[ TypeScript Compiler (tsc) ]
├── 1. Parse into AST
├── 2. Bind symbols & scope
├── 3. Type check & validate constraints
└── 4. Transform & Erase Types
│
▼
JavaScript Output (.js) + Type Declarations (.d.ts)
Note
Types have zero runtime overhead! Types do not exist in the compiled JavaScript bundle. At runtime, performance is 100% equivalent to native JavaScript.
Ensure you have a modern JavaScript runtime installed:
- Node.js: Version 20.x or 22.x LTS
- Package Manager:
npm10+,pnpm9+, orbun1.1+
# Verify installation
node -v # Expected: >= v20.0.0
npm -v # Expected: >= 10.0.0Clone the repository and install all dependencies:
# Clone the repository
git clone https://github.com/TheLearningHubOrg/learn-typescript.git
# Navigate into the project directory
cd learn-typescript
# Install dependencies
npm install
# Run the showcase application
npm startlearn-typescript/
├── .github/ # GitHub Actions CI/CD workflows & guidelines
│ ├── workflows/
│ │ ├── docker.yml # Automated DockerHub builds
│ │ └── releases.yml # Releases & semantic changelog
│ ├── copilot-instructions.md # AI agent instructions
│ └── pull_request_template.md # PR quality checklist
├── .vscode/ # Editor extensions, tasks, launch configs
├── public/ # Favicons, sponsorship banners
├── src/ # TypeScript source code
│ ├── index.ts # Core TypeScript 5 showcase implementations
│ └── index.test.ts # Vitest test suite
├── Dockerfile # Containerization recipe
├── package.json # Dependencies & scripts
├── tsconfig.json # Strict compiler options
├── CHANGELOG.md # Release version history
├── CODE_OF_CONDUCT.md # Contributor covenant
├── CONTRIBUTING.md # How to contribute
├── SECURITY.md # Security vulnerability policy
└── README.md # Comprehensive technical documentation
| Command | Purpose |
|---|---|
npm run dev |
Executes src/index.ts with instant live-reload on save via tsx watch |
npm run build |
Compiles TypeScript into JavaScript and .d.ts declaration files via tsc |
npm start |
Executes the compiled production bundle via node dist/index.js |
npm test |
Runs the Vitest test suite with type checks |
npm run test:watch |
Runs Vitest in interactive watch mode for TDD |
- Scanner & Parser: Converts raw text characters into an Abstract Syntax Tree (AST).
- Binder: Identifies symbols, scopes, declarations, and assigns lexical bindings.
- Type Checker: Evaluates structural type compatibility, computes conditional types, and enforces constraints.
- Emitter: Translates the AST into target JavaScript (
.js), sourcemaps (.js.map), and declarations (.d.ts).
Enabling "strict": true activates a family of fundamental compiler flags that every professional project should enforce:
| Compiler Flag | Behavior when enabled |
|---|---|
noImplicitAny |
Raises an error on any variable or parameter whose type defaults to any implicitly. |
strictNullChecks |
Distinguishes null and undefined from other types. They cannot be assigned to string/number without an explicit union. |
strictFunctionTypes |
Enforces contravariance for function parameter types. |
strictBindCallApply |
Enforces type safety on call, bind, and apply methods. |
strictPropertyInitialization |
Requires class instance properties to be initialized in the constructor or declared optional. |
noImplicitThis |
Raises an error whenever this defaults to any. |
useUnknownInCatchVariables |
Forces catch clause error variables to be typed as unknown instead of any. |
noUncheckedIndexedAccess |
When accessing array elements by index or dictionary keys, returns `T |
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}TypeScript natively supports all JavaScript primitives:
const age: number = 32;
const name: string = 'TypeScript 5';
const isEnabled: boolean = true;
const bigId: bigint = 9007199254740991n;
const uniqueKey: symbol = Symbol('auth');Understanding the hierarchy of types in TypeScript:
unknown / any (Top Types)
│
┌─────────────┴─────────────┐
│ │
string, number, boolean, Object, etc.
│ │
└─────────────┬─────────────┘
│
never (Bottom Type)
any: Opts out of all type checking. Dangerous in production because it disables compile-time safety and spreads unchecked through calling code.unknown: The type-safe counterpart ofany. Any value can be assigned tounknown, but no operations can be performed on it without narrowing first:function processPayload(data: unknown) { // ❌ data.toUpperCase(); // Error: Object is of type 'unknown' if (typeof data === 'string') { console.log(data.toUpperCase()); // ✅ Narrowed to string! } }
never: Represents values that can never occur. Used for functions that never return (e.g. throw error or infinite loop), or unreachable branches in switch statements (exhaustiveness checking).void: Represents the absence of any returned value from a function.
// Literal types limit values to exact constants
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 404 | 500;
// as const prevents widening from literals to general primitives
const config = {
endpoint: 'https://api.github.com',
timeoutMs: 5000,
roles: ['admin', 'editor']
} as const;
// Type of config:
// {
// readonly endpoint: "https://api.github.com";
// readonly timeoutMs: 5000;
// readonly roles: readonly ["admin", "editor"];
// }A tuple is a fixed-length array where the type of each element is known:
// Fixed 3-element tuple
type GeoPoint = [latitude: number, longitude: number, elevation?: number];
const point: GeoPoint = [37.7749, -122.4194];
// Variadic Tuple Types
type StringNumberBooleans = [string, number, ...boolean[]];
const entry: StringNumberBooleans = ['hello', 1, true, false, true];
// Readonly Tuple
const coordinates: readonly [number, number] = [10.5, 42.8];
// coordinates[0] = 20; // ❌ Cannot assign to '0' because it is a read-only property.Both can describe the shape of an object, but they differ in critical ways:
| Feature | Interface | Type Alias |
|---|---|---|
| Declaration Merging | ✅ Allowed (merged automatically) | ❌ Syntax Error (Duplicate identifier) |
| Extends / Inheritance | interface B extends A |
Intersection: type B = A & { ... } |
| Union & Intersection Primitives | ❌ Cannot define unions of primitives | ✅ type ID = string | number |
| Tuple & Mapped Types | ❌ Cannot express mapped types directly | ✅ Full support |
| Compiler Performance | Faster lookup (cached internally by name) | Evaluated recursively |
// Interface Declaration Merging (Widely used to extend global libraries like Express)
interface Window {
myCustomAnalytics: (event: string) => void;
}
// Type Aliases for Union / Computed types
type Status = 'pending' | 'active' | 'archived';
type Tree<T> = { value: T; children: Tree<T>[] };TypeScript uses duck typing (structural typing). If two objects have the same shape, they are treated as having the same type:
interface Point2D {
x: number;
y: number;
}
function printPoint(p: Point2D) {
console.log(`x=${p.x}, y=${p.y}`);
}
const point3D = { x: 10, y: 20, z: 30 };
printPoint(point3D); // ✅ Allowed! point3D has 'x' and 'y' (structural subtyping)
// Excess Property Check on fresh object literals:
// printPoint({ x: 10, y: 20, z: 30 }); // ❌ Error: 'z' does not exist in type 'Point2D'abstract class DatabaseConnection {
// Private identifier (ECMAScript private field)
#secretKey: string;
// TypeScript parameter properties: declare and initialize in one line
constructor(
protected readonly host: string,
protected readonly port: number,
secret: string
) {
this.#secretKey = secret;
}
// Abstract method must be implemented by subclasses
abstract connect(): Promise<void>;
// Public getter
public get connectionString(): string {
return `${this.host}:${this.port}`;
}
}
class PostgresConnection extends DatabaseConnection {
async connect(): Promise<void> {
console.log(`Connected to Postgres at ${this.connectionString}`);
}
}Function overloading allows a single function implementation to support multiple strongly typed calling patterns:
// Overload signatures (must precede implementation)
function formatValue(value: string): string;
function formatValue(value: number, precision: number): string;
function formatValue(value: Date): string;
// Implementation signature (compatible with all overloads)
function formatValue(value: string | number | Date, precision?: number): string {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number') {
return value.toFixed(precision ?? 2);
}
return value.toISOString();
}
formatValue(' hello '); // Returns string
formatValue(42.5678, 2); // Returns string
formatValue(new Date()); // Returns stringTypeScript allows declaring the expected type of this as the first parameter (erased at runtime):
interface CardDeck {
suits: string[];
cards: number[];
createPicker(this: CardDeck): () => { suit: string; card: number };
}
const deck: CardDeck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createPicker(this: CardDeck) {
return () => {
const pickedCard = Math.floor(Math.random() * 52);
const pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit]!, card: pickedCard % 13 };
};
}
};Generics enable writing reusable, componentized functions, classes, and interfaces that work across a variety of types while preserving complete type safety.
// Generic Identity Function
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42); // number
const str = identity('TypeScript 5'); // inferred as "TypeScript 5"
// Generic Interface
interface ApiResponse<TData, TMeta = Record<string, unknown>> {
success: boolean;
data: TData;
meta?: TMeta;
timestamp: string;
}
// 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];
}
}By default, a generic parameter T could be anything. Use extends to restrict acceptable types:
interface HasId {
id: string | number;
}
function printId<T extends HasId>(entity: T): void {
console.log(`Entity ID is: ${entity.id}`);
}
printId({ id: 'user_101', name: 'Alice' }); // ✅ Allowed
// printId({ name: 'Bob' }); // ❌ Property 'id' is missing// Safely extract a property from an object ensuring the key exists
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, username: 'manthanank', email: 'me@example.com' };
const username = getProperty(user, 'username'); // string
// getProperty(user, 'nonExistentKey'); // ❌ Argument of type '"nonExistentKey"' is not assignable to keyof userPrior to TypeScript 5, passing an object literal to a generic function would widen literal types unless as const was manually appended. In TypeScript 5, adding const to the type parameter infers exact literal tuples automatically:
// Without const type parameter: T is inferred as string[]
function getRolesOld<T extends readonly string[]>(roles: T): T {
return roles;
}
const r1 = getRolesOld(['admin', 'viewer']); // string[]
// With TS 5 const type parameter: T is inferred as readonly ["admin", "viewer"]
function getRolesNew<const T extends readonly string[]>(roles: T): T {
return roles;
}
const r2 = getRolesNew(['admin', 'viewer']); // readonly ["admin", "viewer"]TypeScript provides built-in global utility types to facilitate type transformations:
interface User {
id: string;
name: string;
email: string;
age?: number;
}
// 1. Partial<T> - Makes all properties optional
type UpdateUserDto = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number }
// 2. Required<T> - Makes all properties mandatory
type CompleteUser = Required<User>;
// { id: string; name: string; email: string; age: number }
// 3. Readonly<T> - Freezes all properties from reassignment
type ImmutableUser = Readonly<User>;// 4. Pick<T, K> - Constructs a type with only specified keys
type UserCredentials = Pick<User, 'email'>;
// { email: string }
// 5. Omit<T, K> - Constructs a type excluding specified keys
type UserWithoutId = Omit<User, 'id'>;
// { name: string; email: string; age?: number }
// 6. Exclude<UnionType, ExcludedMembers> - Removes types from a union
type AllStatus = 'pending' | 'active' | 'archived' | 'deleted';
type UsableStatus = Exclude<AllStatus, 'archived' | 'deleted'>;
// "pending" | "active"
// 7. Extract<UnionType, Union> - Extracts types from a union
type StringOrNumber = string | number | boolean;
type OnlyString = Extract<StringOrNumber, string>;
// string// 8. Record<Keys, Type> - Constructs an object type with property keys of type Keys and values of Type
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
// 9. NonNullable<T> - Excludes null and undefined from T
type NullableString = string | null | undefined;
type CleanString = NonNullable<NullableString>; // string
// 10. ReturnType<T> - Extracts the return type of a function
function fetchUser() {
return { id: 1, name: 'Alex', active: true };
}
type UserResponse = ReturnType<typeof fetchUser>;
// { id: number; name: string; active: boolean }
// 11. Parameters<T> - Obtains the parameters of a function type in a tuple
type FetchParams = Parameters<(id: string, cache: boolean) => void>;
// [id: string, cache: boolean]
// 12. Awaited<T> - Recursively unwraps Promises
type AsyncData = Promise<Promise<string[]>>;
type ResolvedData = Awaited<AsyncData>; // string[]Mapped types iterate over keys of an existing type to produce a transformed structure:
// Basic Mapped Type
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Modifiers: Adding and Removing readonly and optional (?)
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // Removes readonly modifier
};
type Concrete<T> = {
[K in keyof T]-?: T[K]; // Removes optional modifier
};
// Key Remapping via 'as' (Template Literal Types in Keys)
interface RawUser {
name: string;
age: number;
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<RawUser>;
// {
// getName: () => string;
// getAge: () => number;
// }Conditional types select one of two possible types based on a subtyping relationship:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false
// Distributivity: When conditional types act on a generic naked type parameter over a union,
// they distribute across the union members:
type ToArray<T> = T extends any ? T[] : never;
type UnionArray = ToArray<string | number>;
// Result: string[] | number[] (NOT (string | number)[])
// Preventing Distributivity using square brackets:
type NonDistributiveToArray<T> = [T] extends [any] ? T[] : never;
type FixedArray = NonDistributiveToArray<string | number>;
// Result: (string | number)[]The infer keyword allows deducing a type from within a conditional check:
// Unpack the inner element type of an array or Promise
type Flatten<T> = T extends Array<infer U> ? U : T;
type StrElem = Flatten<string[]>; // string
type NonArr = Flatten<number>; // number
// Extract First Argument of a Function
type FirstArgument<T> = T extends (first: infer A, ...rest: any[]) => any ? A : never;
type F1 = FirstArgument<(name: string, age: number) => void>; // string
// Extract Return Type of a Promise
type UnpackPromise<T> = T extends Promise<infer R> ? R : T;
type P = UnpackPromise<Promise<number>>; // numberTemplate literal types build upon string literal types and can expand into many strings via unions:
type Vertical = 'top' | 'bottom';
type Horizontal = 'left' | 'right';
type Alignment = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
// Event listener names
type Entity = 'user' | 'order';
type Action = 'create' | 'update' | 'delete';
type EventKey = `${Entity}:${Action}`;
// "user:create" | "user:update" | "user:delete" | "order:create" | ...
// Intrinsic String Manipulation Types:
// Uppercase<StringType>, Lowercase<StringType>, Capitalize<StringType>, Uncapitalize<StringType>
type Shout = Uppercase<'hello world'>; // "HELLO WORLD"Introduced in TypeScript 4.9 and enhanced in 5.x, the satisfies operator validates that an expression matches a type without widening or changing the resulting type of that expression:
type Palette = Record<string, string | number[]>;
// ❌ OLD APPROACH (Type Annotation):
// Widens properties to string | number[], losing precise methods
const paletteOld: Palette = {
red: '#ff0000',
green: [0, 255, 0]
};
// paletteOld.red.toUpperCase(); // ❌ Error: Property 'toUpperCase' does not exist on type 'string | number[]'
// ✅ MODERN APPROACH (satisfies):
// Validates structure against Palette, while preserving exact literal types!
const paletteNew = {
red: '#ff0000',
green: [0, 255, 0]
} satisfies Palette;
paletteNew.red.toUpperCase(); // ✅ Works! Type is string!
paletteNew.green.map((v) => v * 2); // ✅ Works! Type is number[]!// Deep Readonly applying immutability to arbitrary nested tree structures
export type DeepReadonly<T> = T extends Function | boolean | number | string | symbol | bigint | null | undefined
? T
: { readonly [K in keyof T]: DeepReadonly<T[K]> };
interface Config {
server: {
host: string;
port: number;
ssl: {
cert: string;
};
};
}
type LockedConfig = DeepReadonly<Config>;
// lockedConfig.server.ssl.cert = 'new'; // ❌ Error: Cannot assign to 'cert' because it is a read-only property.TypeScript’s type checker uses Control Flow Analysis (CFA) to track the precise type of a variable at every line of code as conditionals and branches are executed.
// 1. typeof Type Guard
function padLeft(padding: number | string, input: string): string {
if (typeof padding === 'number') {
return ' '.repeat(padding) + input; // Narrowed to number
}
return padding + input; // Narrowed to string
}
// 2. instanceof Type Guard
class HttpError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handleError(err: unknown) {
if (err instanceof HttpError) {
console.error(`Status ${err.statusCode}: ${err.message}`);
} else if (err instanceof Error) {
console.error(`Generic error: ${err.message}`);
}
}
// 3. in Operator Guard
interface Cat {
meow: () => void;
}
interface Dog {
bark: () => void;
}
function speak(pet: Cat | Dog) {
if ('meow' in pet) {
pet.meow(); // Narrowed to Cat
} else {
pet.bark(); // Narrowed to Dog
}
}When runtime type checks become complex, encapsulate them in custom type guard functions returning parameter is Type:
interface ApiResponseSuccess<T> {
status: 'success';
data: T;
}
interface ApiResponseFailure {
status: 'error';
message: string;
}
type ApiResponse<T> = ApiResponseSuccess<T> | ApiResponseFailure;
// Custom Type Predicate
function isSuccessResponse<T>(res: ApiResponse<T>): res is ApiResponseSuccess<T> {
return res.status === 'success' && 'data' in res;
}
function handleResponse(res: ApiResponse<{ userId: string }>) {
if (isSuccessResponse(res)) {
console.log(`Success! User: ${res.data.userId}`);
} else {
console.error(`Failed with message: ${res.message}`);
}
}Used when a function validates a condition and throws an error if that condition is false, asserting that the variable has narrowed:
function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
if (val === undefined || val === null) {
throw new Error(`Expected 'val' to be defined, but received ${val}`);
}
}
function processUserData(user: { name?: string | null }) {
assertIsDefined(user.name);
// From this point onward in the function, user.name is narrowed to string!
console.log(user.name.toUpperCase());
}Discriminated unions (tagged unions) are one of TypeScript's most powerful architectural patterns:
export type NetworkState =
| { state: 'idle' }
| { state: 'loading' }
| { state: 'success'; response: string }
| { state: 'failed'; code: number; reason: string };
export function renderNetworkState(state: NetworkState): string {
switch (state.state) {
case 'idle':
return 'Waiting to initialize...';
case 'loading':
return 'Loading data from server...';
case 'success':
return `Loaded: ${state.response}`;
case 'failed':
return `Error (${state.code}): ${state.reason}`;
default: {
// If a new state (e.g. 'retrying') is added to NetworkState,
// TypeScript emits a compile-time error here immediately!
const _exhaustiveCheck: never = state;
throw new Error(`Unhandled state: ${_exhaustiveCheck}`);
}
}
}TypeScript 5 supports native ECMAScript Modules via "module": "NodeNext". In this mode, relative import paths must include the .js extension, even in .ts source files:
// Correct modern ESM import
import { calculateArea } from './math.js';
import type { Shape } from './types.js'; // Explicit type importDeclaration files contain only type information and produce no emitted JavaScript:
// globals.d.ts
declare const __API_BASE_URL__: string;
declare const __APP_VERSION__: string;
// Declaring untyped third-party modules or asset files
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}Declaration merging occurs when the compiler joins two separate declarations declared with the same name into a single definition:
// Original interface (e.g., from an npm package)
interface UserProfile {
name: string;
}
// In your application code: Extending UserProfile
interface UserProfile {
roles: string[];
}
// Resulting merged interface:
// interface UserProfile {
// name: string;
// roles: string[];
// }TypeScript 5 introduced support for the official ECMAScript Stage 3 Decorators standard, eliminating the legacy experimentalDecorators flag:
// Method Logger Decorator
function logExecution(
target: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`[EXEC] Calling ${methodName} with args:`, args);
const start = performance.now();
const result = target.call(this, ...args);
const duration = performance.now() - start;
console.log(`[EXEC] ${methodName} completed in ${duration.toFixed(2)}ms`);
return result;
};
}
class PaymentProcessor {
@logExecution
processTransaction(amount: number, currency: string) {
return `Processed ${amount} ${currency}`;
}
}
const processor = new PaymentProcessor();
processor.processTransaction(250, 'USD');Replace runtime exception throwing with explicit, compiler-checked Success/Failure Result types:
export type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
export function Ok<T>(value: T): Result<T, never> {
return { success: true, value };
}
export function Err<E>(error: E): Result<never, E> {
return { success: false, error };
}
// Usage in Business Logic
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return Err('Division by zero is undefined');
}
return Ok(a / b);
}
const res = divide(10, 2);
if (res.success) {
console.log(`Calculation: ${res.value}`); // Narrowed to number
} else {
console.error(`Calculation Failed: ${res.error}`); // Narrowed to string
}type EventMap = {
'user:registered': { userId: string; email: string };
'order:created': { orderId: string; total: number };
'system:alert': { level: 'info' | 'warn' | 'error'; message: string };
};
type EventCallback<T> = (payload: T) => void;
export class TypedEventBus<TMap extends Record<string, unknown>> {
private handlers: { [K in keyof TMap]?: EventCallback<TMap[K]>[] } = {};
on<K extends keyof TMap>(event: K, handler: EventCallback<TMap[K]>): void {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event]!.push(handler);
}
emit<K extends keyof TMap>(event: K, payload: TMap[K]): void {
const list = this.handlers[event];
if (list) {
for (const fn of list) {
fn(payload);
}
}
}
}
// Instantiation:
const bus = new TypedEventBus<EventMap>();
// Compile-time verified events and payloads!
bus.on('user:registered', (data) => {
console.log(`Welcome ${data.email} (${data.userId})`);
});
bus.emit('user:registered', { userId: '123', email: 'user@example.com' });TypeScript is a statically typed syntactic superset of JavaScript developed by Microsoft. All valid JavaScript is valid TypeScript. TypeScript introduces static type definitions that are validated at compile time and completely stripped away (type erasure) to emit standard JavaScript.
number, string, boolean, bigint, symbol, null, and undefined.
anydisables all type checking, permitting arbitrary property access and operations without compile-time warnings.unknownis the type-safe counterpart ofany. Any value can be assigned tounknown, but no properties or operations can be performed on it without narrowing the type first (viatypeof,instanceof, etc.).
TypeScript provides built-in type transformations to manipulate types cleanly. Below is an exhaustive reference with both real-world production usage and how each type is implemented from scratch under the hood.
flowchart TD
TypeT["Original Interface T"] --> Mapped["Mapped Transformations"]
Mapped --> Partial["Partial<T> (All optional)"]
Mapped --> Required["Required<T> (All mandatory)"]
Mapped --> Readonly["Readonly<T> (Immutable)"]
Mapped --> Pick["Pick<T, K> (Subset of keys)"]
Mapped --> Omit["Omit<T, K> (Exclude keys)"]
TypeT --> Conditional["Conditional & Infer Types"]
Conditional --> ReturnType["ReturnType<F> (Extract function return)"]
Conditional --> Parameters["Parameters<F> (Extract args tuple)"]
Conditional --> Awaited["Awaited<P> (Unwrap Promise)"]
Makes all properties optional or mandatory:
interface UserProfile {
id: string;
name: string;
avatarUrl?: string;
bio?: string;
}
// 1. Usage: Updating a profile only requires a subset of fields
function updateUser(id: string, updates: Partial<UserProfile>): void {
console.log(`Updating user ${id} with:`, updates);
}
updateUser('101', { bio: 'Full-stack TypeScript developer' }); // Valid!
// 2. Usage: Ensuring all optional fields are populated for analytics
type CompleteUserProfile = Required<UserProfile>;
// Under the hood implementation:
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type MyRequired<T> = {
[P in keyof T]-?: T[P]; // The '-?' removes optionality!
};Prevents reassigning object properties:
interface DatabaseConfig {
host: string;
port: number;
}
const config: Readonly<DatabaseConfig> = {
host: 'localhost',
port: 5432
};
// config.port = 3306; // Error: Cannot assign to 'port' because it is a read-only property.
// Under the hood implementation:
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// Deep Readonly (Recursive transformation)
type DeepReadonly<T> = T extends Function | boolean | number | string | null | undefined
? T
: T extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: { readonly [P in keyof T]: DeepReadonly<T[P]> };Constructs types by selecting or removing specific property keys:
interface Product {
id: string;
title: string;
description: string;
price: number;
costPrice: number; // Internal secret!
supplierId: string;
}
// Public view picks only safe client fields
type PublicProductView = Pick<Product, 'id' | 'title' | 'description' | 'price'>;
// Internal DTO omits cost price
type SafeProductDto = Omit<Product, 'costPrice'>;
// Under the hood implementation:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;Filters union types:
type SupportedProtocols = 'http' | 'https' | 'ftp' | 'ws' | 'wss';
type SecureProtocols = 'https' | 'wss';
// Exclude: Remove secure protocols from supported protocols
type InsecureProtocols = Exclude<SupportedProtocols, SecureProtocols>; // 'http' | 'ftp' | 'ws'
// Extract: Pick only overlapping types
type OnlySecure = Extract<SupportedProtocols, SecureProtocols>; // 'https' | 'wss'
// Under the hood implementation (Distributive Conditional Types):
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;Extracts types from existing functions without duplicate type declarations:
async function fetchUserOrders(userId: string, limit: number = 10) {
return [
{ orderId: 'ord_1', total: 150.0 },
{ orderId: 'ord_2', total: 89.50 }
];
}
// Extract function return type
type UserOrdersResult = Awaited<ReturnType<typeof fetchUserOrders>>;
// Array<{ orderId: string, total: number }>
// Extract function parameters as a tuple
type FetchOrderArgs = Parameters<typeof fetchUserOrders>;
// [userId: string, limit?: number]
// Under the hood implementation using `infer`:
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;type P1 = Promise<string>;
type R1 = Awaited<P1>; // string
type P2 = Promise<Promise<number>>;
type R2 = Awaited<P2>; // number (Recursively unwrapped!)
// Under the hood implementation:
type MyAwaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: any): any }
? F extends (value: infer V, ...args: any) => any
? MyAwaited<V>
: never
: T;Allows building string types with compile-time pattern verification:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiEndpoint = `/api/v1/${string}`;
type Route = `${HttpMethod} ${ApiEndpoint}`;
const validRoute: Route = 'POST /api/v1/users'; // Valid!
// const invalidRoute: Route = 'PATCH /users'; // Error: Type is not assignable!
// Event Listener Pattern with Capitalization
type EventType = 'click' | 'change' | 'hover';
type EventHandlerName = `on${Capitalize<EventType>}`; // 'onClick' | 'onChange' | 'onHover'In standard TypeScript, strings are structurally compatible (email = userId compiles without warning). Branded types enforce strict type identity at compile time:
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
// Type assertion factories with runtime validation
export function toUserId(id: string): UserId {
if (!id.startsWith('usr_')) throw new Error('Invalid UserId format');
return id as UserId;
}
export function toOrderId(id: string): OrderId {
if (!id.startsWith('ord_')) throw new Error('Invalid OrderId format');
return id as OrderId;
}
function processOrder(userId: UserId, orderId: OrderId) {
console.log(`Processing order ${orderId} for user ${userId}`);
}
const user = toUserId('usr_123');
const order = toOrderId('ord_999');
processOrder(user, order); // Valid!
// processOrder(order, user); // Compile Error: Type 'OrderId' is not assignable to type 'UserId'!Modern TypeScript 5 implements the official TC39 Stage 3 Decorators standard:
// Method Performance Benchmark Decorator
function timing() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
console.log(`[Timing: ${propertyKey}] took ${duration.toFixed(2)}ms`);
return result;
};
return descriptor;
};
}
class PaymentService {
@timing()
async executeCharge(amount: number) {
await new Promise(r => setTimeout(r, 100)); // Simulate payment gateway
return { status: 'PAID', amount };
}
}Extend existing third-party library types safely:
// Augmenting Express Request with authenticated user session
import 'express';
declare module 'express-serve-static-core' {
interface Request {
user?: {
id: string;
email: string;
roles: string[];
};
correlationId: string;
}
}
// Augmenting Global Window object
declare global {
interface Window {
dataLayer: any[];
analyticsConfig: {
apiKey: string;
environment: string;
};
}
}never represents the type of values that never occur. It represents functions that never return (such as infinite loops or functions that unconditionally throw errors) and unreachable conditional branches in exhaustive type checks.
Interfaces can be merged through declaration merging and are optimized for object shapes with inheritance (extends). Type aliases can represent primitives, unions, intersections, tuples, and mapped types, but cannot be reopened or merged.
When enabled, null and undefined are not assignable to other types unless explicitly declared in a union (e.g. string | null). This prevents unexpected TypeError exceptions at runtime.
Type inference is TypeScript’s ability to automatically deduce the type of an expression when no explicit type annotation is provided (e.g., let x = 42; infers x: number).
By appending a question mark ? after the property name:
interface User {
id: string;
bio?: string; // Optional
}- Union types (
A | B) allow a value to be either typeAor typeB. - Intersection types (
A & B) combine multiple types into one, requiring all properties from both types.
Type assertion (value as TargetType) tells the compiler to treat a value as a specific type, overriding the compiler’s deduction. It should be avoided when possible because it bypasses safety checks and can conceal runtime type mismatches.
Structural typing means type compatibility and equality are determined solely by the shape and members of the type, rather than explicit declarations or nominal class hierarchies. If type A has all the members required by type B, A is assignable to B.
A discriminated union is a union of object types where each member has a common literal property (the "discriminant" or "tag", e.g., kind: 'circle' | 'square'). The compiler uses this tag to narrow down the union inside switch or if statements.
keyof T produces a union of string, number, or symbol literal keys belonging to the type T.
interface Point { x: number; y: number; }
type PointKeys = keyof Point; // "x" | "y"Mapped types build new object types by iterating over the keys of an existing type using the [K in Keys] syntax, allowing modifiers like readonly or ? to be added or removed.
The satisfies operator validates that an expression matches a specified type constraint without widening the resulting type or losing the exact literal inferences of its properties.
as const instructs the compiler to infer the narrowest possible literal types, marking all object properties as readonly and converting arrays into fixed-length readonly tuples.
A type predicate has the return signature parameter is Type. When a function returning this predicate evaluates to true, the TypeScript compiler narrows the inspected variable to that type in following code blocks.
noImplicitAny is a single compiler flag that disallows variables defaulting to any. strict is a parent flag that enables noImplicitAny alongside strictNullChecks, strictFunctionTypes, strictBindCallApply, and others.
Pick<T, K>constructs a type by picking only keysKfromT.Omit<T, K>constructs a type by picking all keys fromTand dropping keysK.
infer introduces a temporary type variable within the extends clause of a conditional type, allowing the compiler to extract and deduce an internal type component (such as the return type of a function or the inner value of a Promise).
- Covariance: A type order is preserved:
Dog <: AnimalimpliesArray<Dog> <: Array<Animal>(return types are covariant). - Contravariance: A type order is reversed: with
strictFunctionTypes: true, function parameter types are contravariant ((a: Animal) => void <: (d: Dog) => void). - Bivariance: When
strictFunctionTypes: false, function parameters were bivariant (both covariant and contravariant). - Invariance: Types cannot be substituted in either direction (read-write generic containers).
In a switch statement over a discriminated union, assign the unhandled state to a variable of type never:
default: {
const _exhaustive: never = state;
throw new Error(`Unhandled variant: ${_exhaustive}`);
}If a new member is added to the union later, the compiler immediately flags an error at this line.
Declaring <const T> on a generic parameter causes function arguments to be inferred with as const literal precision automatically, eliminating boilerplate calls to as const at call sites.
NodeNext: Enforces strict Node.js ECMAScript Module rules, requiring explicit file extensions (.js) in relative import paths and honoringexportsandtype: moduleinpackage.json.bundler: Designed for Vite/Webpack environments where file extensions are optional and resolved by external bundler tools rather than Node.js runtime.
Deeply nested recursive conditional types or massive unions can trigger exponential compiler evaluation steps, leading to slow IDE response times and tsc memory exhaustion (JavaScript heap out of memory). Mitigate by using interface caching, indexing lookups instead of unions, and tail-call optimized conditional types.
// 1. Primitive & Union
type ID = string | number;
// 2. Interface with Generics
interface Entity<T> {
id: ID;
data: T;
createdAt: Date;
}
// 3. Const Type Parameter (TS 5)
function createTuple<const T extends readonly unknown[]>(elements: T): T {
return elements;
}
// 4. Mapped Type
type ReadonlyRecord<K extends keyof any, T> = {
readonly [P in K]: T;
};
// 5. Conditional & Infer
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// 6. Discriminated Union
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// 7. Satisfies Operator
const config = {
timeout: 1000,
retries: 3
} satisfies Record<string, number>;{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"declaration": true,
"noUncheckedIndexedAccess": true
}
}Contributions, issues, and feature requests are welcome!
- Fork the Project (
https://github.com/TheLearningHubOrg/learn-typescript/fork) - Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'feat: Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Please make sure to read the Contributing Guide and Code of Conduct.
Manthan Ankolekar
- GitHub: @manthanank
- Website: manthanank.github.io
- LinkedIn: Manthan Ankolekar