TypeScript Cheatsheets
KDP Book Manifest & MetadataClick to Expand & CopyClick to CollapseManifest
Use the copy buttons below to copy metadata verbatim into the Amazon KDP Publishing forms.
- Books > Computers & Technology > Programming > TypeScript
- Books > Computers & Technology > Web Development
TypeScript Cheatsheets
One-Page Quick References from Core Syntax to Advanced Patterns
TypeScript Cheatsheets
First Edition: 2026
Copyright © 2026 by usefulcheatsheets.com. All rights reserved.
No part of this book may be reproduced in any form or by any electronic or mechanical means, including information storage and retrieval systems, without written permission from the publisher, except for the use of brief quotations in a book review.
Table of Contents
A starter guide to TypeScript covering types, variables, and basic syntax for newcomers.
A guide to TypeScript classes, constructors, access modifiers, and inheritance with practical examples.
Use enums effectively for type-safe constants and fixed sets of values.
Use enums effectively for type-safe constants and avoid common pitfalls.
Understand common TypeScript errors and how to fix them.
Learn how interfaces define object shapes, optional properties, and contracts for cleaner typed code.
Learn how to organize TypeScript code using ES modules, named exports, and default exports.
Quick reference for the most important tsconfig.json options to configure your TypeScript projects.
Understand type aliases, unions, and intersections to make your TypeScript code more expressive.
A quick reference for async/await, Promises, error handling, and concurrent operations in TypeScript.
Table of Contents
Write .d.ts files, publish typed npm packages, and create type stubs.
Master generics to build reusable, type-safe functions, classes, and components in TypeScript.
Build flexible but safe types using constrained generics.
A reference for handling null and undefined safely in TypeScript projects.
Understand strict mode benefits, gotchas, and how to enable it progressively.
Master typeof, instanceof, in, and discriminated unions to safely work with union types.
Type React components, props, and hooks safely with TypeScript.
Common TypeScript patterns for React components, hooks, and props.
Learn conditional types, mapped types, template literals, and advanced pattern matching.
Understand decorators, metadata, class transformations, and real-world patterns.
Table of Contents
A reference for declaring multiple type signatures on a single TypeScript function.
Profile code, optimize types, and identify performance bottlenecks in typed applications.
A practical reference for TypeScript's built-in utility types including Partial, Pick, Omit, Record, ReturnType, and how to compose custom utilities.
Welcome to TypeScript
TypeScript is a key topic in Technology development.
This reference book compiles comprehensive cheatsheets covering everything from fundamentals to advanced patterns.
Use this book as a daily reference or read it linearly to build your knowledge.
How to Use This Book
Each page is a visual cheatsheet with core concepts, practical steps, code snippets, and warnings.
TypeScript Basics
A starter guide to TypeScript covering types, variables, and basic syntax for newcomers.
TL;DR
- 01Add static types to your JavaScript variables and functions.
- 02Catch bugs at compile time before code runs.
- 03Use the TypeScript compiler to convert to JavaScript.
Tips
- 01Let TypeScript infer types when the value is obvious to keep your code clean and readable.
Warnings
- 01Avoid the <code>any</code> type whenever possible, since it removes the type safety that TypeScript provides.
TypeScript Basics
(continued)Setup
- Install TypeScript globally with npm to get the
tsccommand.npm install -g typescript - Run
tsc --initto create atsconfig.jsonfile for your project.tsc --init - Save your code in files with the
.tsextension instead of.js. - Compile your TypeScript file by running the compiler with the filename.
tsc filename.ts - Run the generated JavaScript file with Node or in the browser.
TypeScript Basics
(continued)Basic Types
- Use
stringfor text values like names, messages, or any words.const name: string = "Sam"; - Use
numberfor integers and decimals, since there is no separate float type.const age: number = 30; const price: number = 9.99; - Use
booleanfor true and false values in conditions and flags.const isActive: boolean = true; - Use
anyto disable type checking, but try to avoid it.let data: any = 42; data = "now a string"; // allowed - Use
unknownfor safer code when the type is not yet known.
TypeScript Basics
(continued)Variables
- Declare typed variables with an annotation after the variable name.
let username: string = "Sam"; - Skip the annotation and let TypeScript infer the type from the value.
let count = 5; // inferred as number - Use
constfor values that never change, like config keys or fixed numbers.const MAX_RETRIES = 3; - Declare arrays using square brackets or the generic Array form.
const scores: number[] = [10, 20, 30]; const names: Array<string> = ["A", "B"]; - Use tuples for arrays with fixed types and length.
const point: [number, number] = [10, 20];
TypeScript Basics
(continued)Functions
- Add types to parameters and a return type for full safety.
function add(a: number, b: number): number { return a + b; } - Use
voidas the return type for functions that do not return anything.function log(msg: string): void { console.log(msg); } - Mark optional parameters with a question mark after the name.
function greet(name?: string) { return `Hi ${name ?? "friend"}`; } - Set default parameter values directly in the function signature.
function multiply(a: number, b: number = 2) { return a * b; } - Type arrow functions the same way as regular functions for consistency.
TypeScript Basics
(continued)Compiler
- Edit
tsconfig.jsonto control how the TypeScript compiler builds your code. - Enable
"strict": trueto turn on all strict type-checking options at once.{ "compilerOptions": { "strict": true, "target": "ES2022" } } - Use
"target"to choose which JavaScript version the compiler outputs. - Run
tsc --watchto recompile files every time you save changes.tsc --watch - Check the
outDirsetting to control where compiled JavaScript files are saved.
TypeScript Basics
(FAQ)FAQ
Run npm install -g typescript to install the compiler globally, then use tsc --init in your project folder to generate a tsconfig.json with sensible defaults. From there, run tsc to compile your .ts files to JavaScript.
Both can describe object shapes, but interface supports declaration merging and is generally preferred for defining object structures, while type is more flexible and can represent unions, intersections, and primitives. Use interface for class contracts and public APIs, type for everything else.
Mark a parameter optional with a ? suffix (e.g., name?: string), which makes it string | undefined inside the function. For defaults, use standard JavaScript syntax (name: string = 'guest'), which also makes the argument optional at the call site without the undefined type.
unknown is the type-safe alternative: you can assign anything to it, but TypeScript forces you to narrow the type before using it, preventing accidental runtime errors. Reach for unknown when you genuinely don't know the type upfront, such as parsing JSON or handling catch clause errors.
TypeScript widens literal values (e.g., let x = 'hello' becomes string, not 'hello') to allow reassignment. Use const instead of let to keep the literal type, or add as const to freeze an object or array's inferred types to their exact literal values.
TypeScript Classes
A guide to TypeScript classes, constructors, access modifiers, and inheritance with practical examples.
TL;DR
- 01Create classes with typed properties and a constructor.
- 02Control access with public, private, and protected modifiers.
- 03Extend classes to share behavior through inheritance.
Tips
- 01Prefer parameter properties in constructors to cut boilerplate, since they declare and assign in a single line.
Warnings
- 01Use the <code>#</code> prefix for true privacy when needed, since the <code>private</code> keyword is only enforced at compile time.
TypeScript Classes
(continued)Basic Syntax
- Declare a class with the
classkeyword and a PascalCase name.class User { name: string; constructor(name: string) { this.name = name; } } - Declare typed properties at the top of the class body for clarity.
- The constructor runs automatically when you create a new instance.
const sam = new User("Sam"); - Use
thisinside methods to refer to the current instance. - Methods are typed just like regular functions with parameters and return types.
TypeScript Classes
(continued)Constructor Shortcuts
- Add modifiers to constructor parameters to create and assign properties at once.
class User { constructor(public name: string, public age: number) {} } - This shortcut saves you from writing property declarations separately.
- Combine with
readonlyto make the property immutable after construction.class Point { constructor(readonly x: number, readonly y: number) {} } - Use default values in the constructor for optional inputs.
- Parameter properties work with
public,private,protected, andreadonly.
TypeScript Classes
(continued)Access Modifiers
- Use
publicfor properties that anyone can read or change.class User { public name: string = "Sam"; } - Use
privateto hide properties from code outside the class.class Account { private balance: number = 0; } - Use
protectedto allow access from the class and its subclasses only. - The
#prefix gives true runtime privacy that survives compilation.class Counter { #count = 0; next() { return ++this.#count; } } - Default modifier is
publicwhen none is specified.
TypeScript Classes
(continued)Inheritance
- Use the
extendskeyword to create a subclass of another class.class Animal { move() { console.log("moving"); } } class Dog extends Animal { bark() { console.log("woof"); } } - Call
super()inside the constructor to run the parent constructor first.class Cat extends Animal { constructor(public name: string) { super(); } } - Override parent methods by redeclaring them in the subclass.
- Use
super.method()to call the parent method from inside an override. - A class can extend only one parent — use interfaces for multiple contracts.
TypeScript Classes
(continued)Abstract and Static
- Mark a class
abstractto prevent direct instantiation.abstract class Shape { abstract area(): number; } - Subclasses must implement every abstract method to be usable.
- Use
staticto attach properties or methods to the class itself, not instances.class MathHelper { static double(n: number) { return n * 2; } } MathHelper.double(5); // 10 - Static methods are great for utility functions related to the class.
- Use
implementsto require a class to match an interface contract.
TypeScript Classes
(FAQ)FAQ
Abstract classes can contain implemented methods and constructor logic, while interfaces only define shape contracts with no implementation. Use abstract classes when you want to share code across subclasses; use interfaces when you only need to enforce a structure.
Static members belong to the class itself rather than any instance, so you access them via the class name (e.g., MyClass.count). They're useful for factory methods, counters, or shared utilities that don't depend on instance state.
Use protected when subclasses need direct access to a member, and private when you want to completely hide implementation details from child classes. Choosing protected too liberally can couple subclasses tightly to parent internals, making refactoring harder.
Use super() in the subclass constructor before accessing this, and super.methodName() to invoke an overridden parent method. Forgetting to call super() in a derived constructor is a runtime error, not just a type error.
Yes — a class can implement multiple interfaces by separating them with commas (e.g., class Foo implements Bar, Baz). This is the standard way to satisfy multiple contracts since TypeScript only supports single class inheritance.
TypeScript Enums
Use enums effectively for type-safe constants and fixed sets of values.
TL;DR
- 01Prefer string enums over numeric ones for self-documenting, debuggable code.
- 02Numeric enums support reverse mapping but can be confusing.
- 03Const enums inline values at compile time for smaller bundles.
Tips
- 01Use string enums or union types for new code — they're clearer and don't have the reverse-mapping pitfalls of numeric enums.
Warnings
- 01Numeric enums can cause subtle bugs with reverse mapping — avoid them unless you specifically need the numeric values.
TypeScript Enums
(continued)String Enums
Create a string enum to assign readable values to each member.
enum Status { Active = "active", Inactive = "inactive", Pending = "pending" } const userStatus: Status = Status.Active; // "active"Use string enums in switch statements for exhaustive, readable checks.
function describe(status: Status): string { switch (status) { case Status.Active: return "User is active"; case Status.Inactive: return "User is inactive"; case Status.Pending: return "Awaiting approval"; } }Match API or database string values directly using string enum members.
// API returns { status: "active" } — enum value matches exactly const fromApi: Status = "active" as Status;Iterate over all string enum values using Object.values.
const allStatuses = Object.values(Status); // ["active", "inactive", "pending"]Use a string enum to keep role names self-documenting in conditions.
enum Role { Admin = "admin", User = "user", Guest = "guest" } function checkRole(role: Role) { if (role === Role.Admin) console.log("is admin"); // "admin" in logs }
TypeScript Enums
(continued)Numeric Enums
Create a numeric enum — members auto-increment from zero by default.
enum Level { Low, // 0 Medium, // 1 High // 2 }Set explicit values to control what numbers each member maps to.
enum Direction { Up = 0, Right = 1, Down = 2, Left = 3 } const dir: Direction = Direction.Up; // 0Use reverse mapping to look up a member name from its numeric value.
enum Status { Active = 1, Inactive = 2 } console.log(Status[1]); // "Active" console.log(Status.Active); // 1Use bit flags with left-shift to combine permissions in a single number.
enum Permission { None = 0, Read = 1 << 0, // 1 Write = 1 << 1, // 2 Admin = 1 << 2 // 4 } const userPerms = Permission.Read | Permission.Write; // 3Start auto-increment from 1 to avoid zero-falsy truthiness bugs.
enum Priority { Low = 1, // starts at 1, not 0 Medium, // 2 High // 3 }
TypeScript Enums
(continued)Const Enums
Declare a const enum to inline every member as a literal at compile time.
const enum Color { Red = "red", Green = "green", Blue = "blue" } const myColor: Color = Color.Red; // Compiles to: const myColor = "red";Verify zero runtime overhead — no JS object is generated in the bundle.
const enum Direction { Up, Down, Left, Right } const dir = Direction.Up; // Compiles to: const dir = 0; — no Direction object exists at runtimeShow the size difference between regular and const enums.
// Regular enum: creates a runtime object (adds ~200 bytes) // Const enum: replaces each use with its literal value (adds 0 bytes) const enum Size { Small = "sm", Large = "lg" } const s: Size = Size.Small; // becomes: const s = "sm";Use string const enums safely when sharing across module boundaries.
// Safe for cross-module use with string values const enum HttpMethod { Get = "GET", Post = "POST", Put = "PUT" }Avoid const enums in .d.ts library files — consumers cannot inline them.
// In a .d.ts file, prefer a union type or regular enum export type HttpMethod = "GET" | "POST" | "PUT";
TypeScript Enums
(continued)Comparing with Union Types
Use union types for simple value sets — they need no runtime code.
type Status = "active" | "inactive" | "pending"; const status: Status = "active"; // direct string, no enum import neededUse an as const object to get both runtime values and a derived type.
const STATUSES = { Active: "active", Inactive: "inactive" } as const; type Status = typeof STATUSES[keyof typeof STATUSES]; // Status = "active" | "inactive"Prefer union types when you need to serialize values to JSON cleanly.
// Simpler, no runtime object, serializes as a plain string type Direction = "up" | "down" | "left" | "right";Use enums when you need a named constant group importable as one symbol.
// Useful when the group name matters for readability and logging enum LogLevel { Debug = "debug", Info = "info", Error = "error" } function log(level: LogLevel, msg: string) { console.log(`[${level}] ${msg}`); }Combine an as const object with Object.values to iterate like an enum.
const Roles = { Admin: "admin", User: "user", Guest: "guest" } as const; type Role = typeof Roles[keyof typeof Roles]; // "admin" | "user" | "guest" Object.values(Roles).forEach(r => console.log(r));
TypeScript Enums
(continued)Common Patterns
Use a numeric enum for HTTP status codes to keep values readable.
enum HttpStatus { Ok = 200, Created = 201, BadRequest = 400, Unauthorized = 401, NotFound = 404 }Write a type guard to check if a runtime value is a valid enum member.
function isValidStatus(value: unknown): value is Status { return Object.values(Status).includes(value as Status); }Export all enums from a central file to share them across modules.
// enums.ts export enum UserRole { Admin = "admin", User = "user" } // other-file.ts import { UserRole } from "./enums";Use Record with an enum type to create exhaustive display label maps.
const statusLabels: Record<Status, string> = { [Status.Active]: "Active", [Status.Inactive]: "Inactive", [Status.Pending]: "Pending" }; console.log(statusLabels[Status.Active]); // "Active"Use enums as discriminants in discriminated union types for events.
enum EventType { Click = "click", KeyPress = "keypress" } type AppEvent = | { type: EventType.Click; x: number; y: number } | { type: EventType.KeyPress; key: string };
TypeScript Enums
(FAQ)FAQ
Prefer union types like type Direction = 'north' | 'south' for simple sets of values — they require no runtime code and are easier to extend. Use string enums when you want a named, iterable group of constants that appear as a single importable symbol across your codebase.
Numeric enums generate reverse mappings at runtime, so Object.keys(MyEnum) returns both the names and their numeric values (e.g., ['Up', 'Down', '0', '1']). Filter with isNaN or switch to string enums to avoid this pitfall.
A const enum is erased entirely at compile time — all usages are replaced with their literal values, producing no JavaScript object. Use it when bundle size matters and you don't need to iterate over or import the enum at runtime, but note it's incompatible with isolatedModules mode.
No — enums only support constant or computed member values, not methods. If you need behavior attached to enum values, use a plain object with as const or a class with static members instead.
Cast the enum to Record<string, string> and use Object.values: Object.values(MyEnum).includes(value as MyEnum). TypeScript won't narrow this automatically, so you'll need a type guard function that returns value is MyEnum for safe use downstream.
TypeScript Enums Best Practices
Use enums effectively for type-safe constants and avoid common pitfalls.
TL;DR
- 01Use string enums for better debugging and serialization.
- 02Use const enums to avoid runtime overhead.
- 03Prefer union types over enums for simpler type definitions.
Tips
- 01Use union types with <code>as const</code> instead of enums for most cases — they're simpler and don't have the pitfalls of enum reverse mapping.
Warnings
- 01Numeric enums can cause subtle bugs due to reverse mapping — prefer string enums or union types to avoid confusion.
TypeScript Enums Best Practices
(continued)String Enums
- Create enums with string values for clarity.
enum Status { Active = "active", Inactive = "inactive", Pending = "pending" } const userStatus: Status = Status.Active; - String enums are self-documenting and easier to debug.
// Logs "active" instead of "0" — readable in console and logs console.log(Status.Active); // "active" - Values are explicit and match database/API values.
enum Role { Admin = "admin", User = "user", Guest = "guest" } function checkRole(role: Role) { if (role === Role.Admin) { // user is admin } } - Use string enums in switch statements for exhaustive type checking.
function describe(s: Status): string { switch (s) { case Status.Active: return "Active"; case Status.Inactive: return "Inactive"; case Status.Pending: return "Pending"; } } - Iterate string enum values safely with Object.values.
const values = Object.values(Status); // ["active", "inactive", "pending"]
TypeScript Enums Best Practices
(continued)Numeric Enums
- Create enums with numeric values for flexibility.
enum Direction { Up = 0, Right = 1, Down = 2, Left = 3 } const dir: Direction = Direction.Up; - Numeric enums support reverse mapping.
enum Status { Active = 1, Inactive = 2 } console.log(Status[1]); // "Active" console.log(Status.Active); // 1 - Auto-incrementing values reduce boilerplate.
enum Level { Low, // 0 Medium, // 1 High // 2 } - Start numeric enums at 1 to avoid falsy zero bugs.
enum Priority { Low = 1, // avoids if (priority) being false for Low Medium, // 2 High // 3 } - Use bit flags with numeric enums for combined permissions.
enum Permission { None = 0, Read = 1 << 0, // 1 Write = 1 << 1, // 2 Admin = 1 << 2 // 4 } const canEdit = Permission.Read | Permission.Write; // 3
TypeScript Enums Best Practices
(continued)Const Enums
- Use const for enums that are inlined at compile time.
const enum Color { Red = "red", Green = "green", Blue = "blue" } const myColor: Color = Color.Red; - Const enums don't generate runtime code.
const enum Direction { Up, Down, Left, Right } const d = Direction.Up; // Compiles to: const d = 0; — no enum object in bundle - Smaller bundle size and faster performance.
// Compiles to: const myColor = "red"; - Only string values work reliably with const enums.
// Use string values to stay safe across modules and bundlers const enum HttpMethod { Get = "GET", Post = "POST" } - Avoid const enums in published library declaration files.
// Library consumers can't use const enums from .d.ts files // Use a regular enum or union type instead export type HttpMethod = "GET" | "POST" | "PUT";
TypeScript Enums Best Practices
(continued)Heterogeneous Enums
- Mix string and numeric values in a single enum.
enum Mixed { No = 0, Yes = "YES" } - Avoid heterogeneous enums — they are hard to reason about.
// Confusing: mixing types makes iteration and reverse lookup unpredictable enum Bad { A = 0, B = "b" } - Use separate enums or union types instead for clarity.
// Better: separate enums enum NumericStatus { Inactive = 0, Active = 1 } enum StringStatus { Inactive = "inactive", Active = "active" } - Keep all enum members the same type to enable consistent iteration.
// Safe: all strings — Object.values returns only the values enum Color { Red = "red", Green = "green", Blue = "blue" } const colors = Object.values(Color); // ["red", "green", "blue"] - TypeScript emits a warning for most heterogeneous patterns.
// Prefer string or numeric uniformly to avoid compiler warnings enum Uniform { A = "a", B = "b", C = "c" } // clean
TypeScript Enums Best Practices
(continued)Union Types Alternative
- Use union types instead of enums for simpler definitions.
type Status = "active" | "inactive" | "pending"; const status: Status = "active"; - Union types are simpler and don't generate runtime code.
// No object created — pure compile-time type type Role = "admin" | "user" | "guest"; - Union types are more flexible for type checking and inference.
type Role = "admin" | "user" | "guest"; function hasAccess(role: Role): boolean { return role === "admin" || role === "user"; } - Use as const to create both a type and runtime constants.
const STATUSES = { Active: "active", Inactive: "inactive" } as const; type Status = typeof STATUSES[keyof typeof STATUSES]; // Status = "active" | "inactive" - Prefer union types when values are already plain strings in your codebase.
// API returns "GET" | "POST" directly — no enum needed type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; function request(method: HttpMethod, url: string) { }
TypeScript Enums Best Practices
(FAQ)FAQ
Use string enums whenever enum values will appear in API payloads, logs, or serialized data — Status.Active staying as "active" at runtime is far easier to trace than 0. String enums also avoid the reverse-mapping behavior that numeric enums generate, keeping the compiled output clean.
A const enum is inlined at compile time — TypeScript replaces every usage with the literal value and emits no runtime object, reducing bundle size. However, they break under isolatedModules (used by Babel, esbuild, and Vite), so avoid them in library code or projects with those bundler setups.
Define an object with as const, then derive the value type using indexed access: const Status = { Active: 'active', Inactive: 'inactive' } as const; type Status = typeof Status[keyof typeof Status];. You get full autocomplete and type narrowing with zero runtime overhead and no reverse-mapping surprises.
TypeScript compiles numeric enums with both forward (Name → number) and reverse (number → Name) entries in the same object, so Object.keys(MyEnum) yields both string names and their numeric counterparts. Filter out entries where Number(key) is not NaN before iterating, or switch to string enums which skip the reverse mapping entirely.
No — heterogeneous enums combine the downsides of both types (reverse mapping from numeric members, inconsistent value shapes) while providing no advantage over two separate typed constants. Split them into distinct string enums or union types based on what each group of values actually represents.
TypeScript Error Messages
Understand common TypeScript errors and how to fix them.
TL;DR
- 01Type mismatch errors indicate your value doesn't match the declared type.
- 02Cannot find name errors mean the variable doesn't exist or isn't imported.
- 03Property doesn't exist errors indicate misspelled properties or missing types.
Tips
- 01Read the full error message carefully — TypeScript provides helpful suggestions for how to fix the error.
Warnings
- 01Avoid using <code>any</code> or <code>as any</code> to silence errors — fix the underlying type issues instead for safer code.
TypeScript Error Messages
(continued)Type Mismatch Errors
- Type is not assignable to parameter type.
function greet(name: string) { console.log(`Hello ${name}`); } greet(123); // error: number is not assignable to string // Fix: greet("Alice"); - Property is missing from type.
interface User { name: string; age: number; } const user: User = { name: "Alice" }; // error: property 'age' is missing // Fix: add age property - Property value has wrong type.
interface User { age: number; } const user: User = { age: "30" }; // error: string is not assignable to number // Fix: change age to 30 (without quotes) - Union type is too broad for the required narrow type.
function setStatus(status: "active" | "inactive") { } const s: string = "active"; setStatus(s); // error: string is not assignable // Fix: type s as the exact union or use a literal setStatus(s as "active" | "inactive"); - Return type does not match function's declared type.
function getId(): number { return "abc"; // error: string is not assignable to number } // Fix: return a number function getId(): number { return 42; }
TypeScript Error Messages
(continued)Cannot Find Name Errors
- Variable is not defined in scope.
console.log(userName); // error: Cannot find name 'userName' // Fix: declare the variable first const userName = "Alice"; console.log(userName); - Import is missing or misspelled.
const express = require("express"); app.listen(); // error: cannot find name 'app' // Fix: create app instance const app = express(); - Variable is out of scope due to block scoping.
if (true) { const x = 5; } console.log(x); // error: x is out of scope // Fix: move console.log inside the if block or use let/var - Type or interface is used but never imported.
const user: User = { name: "Alice" }; // error: Cannot find name 'User' // Fix: add the import import { User } from "./types"; - Global type not available because lib config is missing.
// error: Cannot find name 'Promise' // Fix: add "es2015" or "es6" to lib in tsconfig.json // { "compilerOptions": { "lib": ["es6", "dom"] } } const p: Promise<void> = Promise.resolve();
TypeScript Error Messages
(continued)Property Does Not Exist Errors
- Property is misspelled.
interface User { name: string; } const user: User = { name: "Alice" }; console.log(user.Name); // error: Property 'Name' does not exist // Fix: use lowercase 'name' - Property exists but type is not declared in the interface.
const user = { name: "Alice" }; console.log(user.age); // error: property 'age' does not exist // Fix: add age to the type interface User { name: string; age: number; } - Object might be null or undefined before access.
const user: User | null = null; console.log(user.name); // error: object is possibly null // Fix: add null check if (user) { console.log(user.name); } - Accessing a property on a union type not shared by all members.
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }; function area(s: Shape) { return s.radius; // error: 'radius' does not exist on 'square' // Fix: narrow with a type guard if (s.kind === "circle") return Math.PI * s.radius ** 2; } - Dynamic property key not present in the index signature.
const map: Record<string, number> = {}; const val: number = map["key"]; // may be undefined // Fix: check first or use optional chaining const val2 = map["key"] ?? 0;
TypeScript Error Messages
(continued)Function Errors
- Function arguments don't match parameters.
function add(a: number, b: number): number { return a + b; } add(1); // error: 1 argument provided but 2 required // Fix: provide both arguments add(1, 2); - Return type doesn't match function signature.
function getName(): string { return 123; // error: number is not assignable to string } // Fix: return a string function getName(): string { return "Alice"; } - Value is not callable because it is not a function.
const x = 5; x(); // error: This expression is not callable // Fix: only call variables that are functions - Async function return type is missing the Promise wrapper.
async function loadUser(): User { // error: missing Promise return fetchUser(1); } // Fix: wrap return type in Promise async function loadUser(): Promise<User> { return fetchUser(1); } - Too many arguments passed to a function.
function greet(name: string) { } greet("Alice", "extra"); // error: Expected 1 arguments, but got 2 // Fix: remove extra argument or add a parameter
TypeScript Error Messages
(continued)Fixing with Type Assertions
- Use as to tell TypeScript the actual type.
const value: unknown = "hello"; const length = (value as string).length; // OK - Use type guards to narrow types safely without assertions.
const value: string | number = "hello"; if (typeof value === "string") { console.log(value.length); // OK — TypeScript knows it's a string } - Use non-null assertion (!) only when you are certain the value exists.
const user: User | null = getUser(); console.log(user!.name); // tells TypeScript user is not null // Only use if you're certain the value is not null - Use satisfies to validate a value without widening its type.
const config = { host: "localhost", port: 3000 } satisfies Record<string, string | number>; // config.port is still typed as number, not string | number - Cast through unknown when TypeScript won't accept a direct assertion.
const value = getAnything() as unknown as SpecificType; // Use sparingly — only when you know the runtime type is correct
TypeScript Error Messages
(FAQ)FAQ
This error means the variable or parameter expects a specific type but received a different one — check where the variable is declared and ensure the value you're passing matches that declared type. Often the fix is correcting the data source or updating the type annotation to reflect the actual shape of the data.
The variable is either out of scope, not imported, or not declared before use — add the appropriate import statement or move the declaration above where it's used. For global types like 'process' or 'document', you may need to install the corresponding @types package.
This usually means you're accessing a property that TypeScript doesn't know about — either you've mistyped the property name or the type definition is incomplete. Fix it by correcting the typo, extending the interface with the missing property, or using a type guard to narrow the type before accessing it.
TypeScript checks that function signatures are compatible — if you're passing a callback, ensure the parameter types and count match what the caller expects. When a callback receives more arguments than your function uses, TypeScript is usually fine with that, but missing required parameters will trigger an error.
Type assertions are safe when you have information TypeScript can't infer — such as after a runtime check confirms the shape of parsed JSON or a DOM query result. Avoid asserting to an unrelated type or using it to bypass a legitimate mismatch, as this shifts the type-checking burden entirely to you.
TypeScript Interfaces
Learn how interfaces define object shapes, optional properties, and contracts for cleaner typed code.
TL;DR
- 01Describe the shape of objects using interface declarations.
- 02Mark properties as optional or readonly when needed.
- 03Extend interfaces to reuse and combine type definitions.
Tips
- 01Use PascalCase for interface names and skip the old <code>I</code> prefix, since modern TypeScript style avoids it.
Warnings
- 01Declaration merging can cause unexpected behavior, so avoid reusing interface names across unrelated files.
TypeScript Interfaces
(continued)Basic Syntax
- Declare an interface using the
interfacekeyword and a PascalCase name.interface User { id: number; name: string; } - Use the interface as a type annotation when declaring variables.
const user: User = { id: 1, name: "Sam" }; - List each property with its name and type inside curly braces.
- Add semicolons or commas between properties — both are valid.
- Group related properties into one interface to describe a single object shape.
TypeScript Interfaces
(continued)Property Modifiers
- Mark a property as optional using a question mark after its name.
interface User { id: number; email?: string; } - Use the
readonlykeyword to prevent a property from being changed later.interface Point { readonly x: number; readonly y: number; } - Combine
readonlywith optional properties for safe configuration objects. - Use index signatures for objects with dynamic keys of the same type.
interface Scores { [name: string]: number; } - Set required properties without modifiers, since required is the default.
TypeScript Interfaces
(continued)Methods
- Define methods inside an interface using the shorthand call syntax.
interface Greeter { greet(name: string): string; } - Add parameter types and return types just like regular function declarations.
- Use arrow function style with a property and function type.
interface Greeter { greet: (name: string) => string; } - Both syntax styles work — pick one and stay consistent across your project.
- Type method overloads by listing multiple call signatures inside the interface.
TypeScript Interfaces
(continued)Extending
- Extend an interface using the
extendskeyword to inherit its properties.interface Animal { name: string; } interface Dog extends Animal { breed: string; } - Extend multiple interfaces by listing them separated by commas.
interface Hybrid extends Animal, Trainable {} - Override inherited properties by redeclaring them with a more specific type.
- Use extension to build small base interfaces into larger composed shapes.
- Merge two interfaces with the same name automatically through declaration merging.
TypeScript Interfaces
(continued)Interface vs Type
- Use interfaces for object shapes and class contracts in most cases.
interface User { id: number; name: string } - Use type aliases for unions, intersections, primitives, and tuples instead.
type Status = "loading" | "success" | "error"; - Only interfaces support declaration merging across multiple files.
- Both support extension, but interfaces use
extendsand types use intersections. - Pick one style per project to keep your codebase consistent.
TypeScript Interfaces
(FAQ)FAQ
Prefer interfaces when defining object shapes, especially if the type may need to be extended or merged later. Use type aliases for unions, intersections, primitives, or tuples where interfaces can't express the shape.
Mark a property optional with a question mark (e.g., name?: string) and read-only with the readonly keyword (e.g., readonly id: number). These can be combined on the same property.
Yes, an interface can extend multiple interfaces in a single declaration using a comma-separated list: interface C extends A, B {}. This merges all properties from both parent interfaces into the new one.
Declaration merging automatically combines two interfaces with the same name in the same scope into one. This becomes a problem when two unrelated files both define an interface with the same name, silently adding unexpected properties to each other's shapes.
You can define a method either as a property with a function type (greet: (name: string) => void) or using shorthand method syntax (greet(name: string): void). Both are valid, but the shorthand form is more concise and commonly preferred.
TypeScript Modules
Learn how to organize TypeScript code using ES modules, named exports, and default exports.
TL;DR
- 01Use export and import to share code between files.
- 02Choose named exports for multiple values per module.
- 03Use type-only imports to keep runtime bundles small.
Tips
- 01Prefer named exports over default exports, since they keep import names consistent and make refactoring much safer.
Warnings
- 01Path aliases require both <code>tsconfig.json</code> and your bundler to agree, or imports will work in the IDE but fail at runtime.
TypeScript Modules
(continued)Named Exports
- Add the
exportkeyword in front of any declaration to share it.// utils.ts export const PI = 3.14159; export function double(n: number) { return n * 2; } - Import named exports using curly braces and the exact names.
import { PI, double } from "./utils"; - Rename imports inline with the
askeyword to avoid conflicts.import { double as multiplyByTwo } from "./utils"; - Export multiple values from one file for related helpers and constants.
- This is the most common style in modern TypeScript codebases.
TypeScript Modules
(continued)Default Exports
- Use
export defaultto mark one main export per file.// logger.ts export default function log(msg: string) { console.log(msg); } - Import the default with any name you choose, without curly braces.
import log from "./logger"; - A file can have one default export and any number of named exports.
- Default exports lose the original name during import, which hurts refactoring.
- Many style guides recommend named exports over defaults for that reason.
TypeScript Modules
(continued)Re-exports
- Re-export from another file to build a barrel that groups related modules.
// index.ts export { double, PI } from "./utils"; export { default as log } from "./logger"; - Use
export * fromto re-export every named export from a module.export * from "./utils"; - Barrels make imports cleaner across the rest of your codebase.
- They can also hurt tree-shaking, so use them carefully in large projects.
- Place barrel files at the root of folders for predictable import paths.
TypeScript Modules
(continued)Type-only Imports
- Use the
typekeyword to import only types, not runtime values.import type { User } from "./types"; - Type-only imports are erased during compilation for smaller bundles.
- Mix value and type imports in one statement using the inline
typekeyword.import { type User, getUser } from "./api"; - Use
export typeto re-export types without runtime code.export type { User } from "./types"; - This is especially helpful when working with bundlers and isolated modules.
TypeScript Modules
(continued)Module Resolution
- Use relative paths like
./utilsfor files inside your project. - Use bare specifiers like
reactfor packages installed innode_modules. - Configure path aliases in
tsconfig.jsonto shorten deep import paths.import { Button } from "@/components/Button"; - The
moduleResolutionsetting controls how the compiler finds modules. - Use
"bundler"mode in modern projects that rely on tools like Vite or Webpack.
TypeScript Modules
(FAQ)FAQ
Named exports let you export multiple values from one file and must be imported using their exact name (or an alias). Default exports allow only one per file and can be imported under any name, which makes them harder to track during refactoring.
Use export * from './module' to re-export all named exports, or export { foo, bar } from './module' to selectively re-export. This is common in index files to create a clean public API for a directory.
Use import type { Foo } from './foo' whenever you only need a type for annotations or generics—TypeScript erases it at compile time, so it never appears in the emitted JavaScript. This keeps bundle size smaller and avoids circular-dependency issues caused by value imports.
TypeScript path aliases in tsconfig.json only affect type-checking; bundlers like webpack, Vite, or esbuild need their own alias configuration to resolve them at runtime. Add matching aliases in your bundler config (e.g., resolve.alias in Vite) to make both environments agree.
You can export both simultaneously: export const Status = { Active: 'active' } as const; export type Status = typeof Status[keyof typeof Status];. Consumers can then import the runtime value and the type from the same path without conflict.
TypeScript Tsconfig Options
Quick reference for the most important tsconfig.json options to configure your TypeScript projects.
TL;DR
- 01Use strict mode to catch the most type-related bugs.
- 02Set target and module to match your runtime environment.
- 03Configure include and exclude to control compiled files.
Tips
- 01Start every new project with <code>"strict": true</code>, since adding it later means fixing many type errors at once.
Warnings
- 01Changing <code>"target"</code> or <code>"module"</code> mid-project can break your bundler, so update one setting at a time and test.
TypeScript Tsconfig Options
(continued)Getting Started
- Generate a starter file by running the compiler with the init flag.
tsc --init - The
tsconfig.jsonfile sits at the root of your TypeScript project. - All compiler options live under the
"compilerOptions"key.{ "compilerOptions": { "strict": true } } - Run
tscwith no arguments to compile using these settings. - Use
tsc --noEmitfor type-checking only without writing files.
TypeScript Tsconfig Options
(continued)Strict Mode
- Enable
"strict": trueto turn on all strict checks at once.{ "compilerOptions": { "strict": true } } - Strict mode includes
noImplicitAny,strictNullChecks, and several others. - Use
"strictNullChecks"to require explicit handling of null and undefined. - Use
"noImplicitAny"to flag variables that have no type annotation. - Strict mode is the recommended baseline for every new project.
TypeScript Tsconfig Options
(continued)Target and Module
- Use
"target"to choose the JavaScript version of the compiled output.{ "compilerOptions": { "target": "ES2022" } } - Pick
"ES2022"or newer for modern browsers and current Node versions. - Use
"module"to choose between ESM, CommonJS, or bundler modules.{ "compilerOptions": { "module": "ESNext" } } - Use
"lib"to include type definitions for browser APIs or other globals. - These settings determine what features your compiled code can use.
TypeScript Tsconfig Options
(continued)Paths and Includes
- Use
"include"and"exclude"to control which files are compiled.{ "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } - Use
"outDir"to choose where compiled JavaScript files are saved. - Use
"rootDir"to set the root folder of your TypeScript source files. - Set up path aliases with
"baseUrl"and"paths"for cleaner imports.{ "compilerOptions": { "baseUrl": "./src", "paths": { "@/*": ["*"] } } } - Path aliases require both
tsconfig.jsonand your bundler to agree.
TypeScript Tsconfig Options
(continued)Helpful Extras
- Enable
"sourceMap": trueto generate source maps for easier debugging.{ "compilerOptions": { "sourceMap": true } } - Use
"esModuleInterop": truefor cleaner imports from CommonJS modules. - Set
"skipLibCheck": trueto skip type checking of declaration files. - Use
"resolveJsonModule": trueto import JSON files directly in code. - Use
"jsx": "preserve"for React projects that use a separate bundler.
TypeScript Tsconfig Options
(FAQ)FAQ
The 'strict' flag enables a group of checks including 'strictNullChecks', 'noImplicitAny', 'strictFunctionTypes', and others. Enabling it together is more reliable than toggling individual flags, since some checks depend on others being active.
Use the 'include' array to specify which directories or globs TypeScript should compile, and 'exclude' to block specific paths like 'node_modules' or 'dist'. Without 'include', TypeScript compiles every .ts file it finds from the project root.
'target' controls the JavaScript syntax version TypeScript outputs (e.g. ES2020), while 'module' controls how imports and exports are emitted (e.g. CommonJS vs ESNext). They are independent—you can target ES5 syntax while still emitting ESNext modules.
Add a 'paths' mapping under 'compilerOptions' and set 'baseUrl' to your project root. Note that TypeScript only handles type resolution—your bundler (Webpack, Vite, etc.) needs its own alias config to resolve these paths at runtime.
TypeScript only uses the nearest tsconfig.json relative to the file being compiled, so nested configs can shadow parent ones. Also, running 'tsc' directly without a file argument reads tsconfig.json, but passing a file explicitly (e.g. 'tsc index.ts') bypasses it entirely.
TypeScript Type Aliases
Understand type aliases, unions, and intersections to make your TypeScript code more expressive.
TL;DR
- 01Create reusable type names with the type keyword.
- 02Combine types using unions and intersections for flexibility.
- 03Lock values to literals for precise type safety.
Tips
- 01Use type aliases for unions and intersections, and use interfaces for object shapes you may want to extend later.
Warnings
- 01Type aliases cannot be merged like interfaces, so duplicate names will cause a compiler error.
TypeScript Type Aliases
(continued)Basic Syntax
- Declare a type alias using the
typekeyword and PascalCase naming.type User = { id: number; name: string; }; - Assign any valid type, including objects, primitives, or other type aliases.
type ID = number | string; - Use the alias anywhere you would normally write the full type expression.
const user: User = { id: 1, name: "Sam" }; - Type aliases can describe object shapes much like interfaces can.
- Keep aliases short and focused to make them easy to reuse.
TypeScript Type Aliases
(continued)Union Types
- Combine types with the pipe symbol for multiple allowed options.
type ID = string | number; const a: ID = "abc"; const b: ID = 42; - Use unions to allow a variable to hold values of different shapes.
- Narrow union types inside code using
typeoforinchecks.function format(value: string | number) { if (typeof value === "string") return value.trim(); return value.toFixed(2); } - Build status types from string literals for clear, restricted state.
type Status = "loading" | "success" | "error"; - Pass unions as function parameters to accept several valid input types.
TypeScript Type Aliases
(continued)Intersection Types
- Combine multiple types into one using the ampersand symbol between them.
type Named = { name: string }; type Aged = { age: number }; type Person = Named & Aged; - Intersections merge all properties from each type into a single shape.
const sam: Person = { name: "Sam", age: 30 }; - Use intersections to compose small types into larger object definitions.
- Conflicting property types in intersections create unusable
nevervalues. - Prefer intersections over interface extension when working with type aliases.
TypeScript Type Aliases
(continued)Literal Types
- Restrict a value to one exact string, number, or boolean literal.
type Direction = "up" | "down" | "left" | "right"; - Combine literal types with unions to create fixed sets of allowed values.
type Size = "sm" | "md" | "lg"; - Use literals for things like API methods, sizes, or status codes.
- Add
as constto objects to make all properties literal and readonly.const CONFIG = { mode: "prod" } as const; - Literal types catch typos that regular string types would silently allow.
TypeScript Type Aliases
(continued)Advanced Patterns
- Use generic type aliases for flexible, reusable type shapes.
type Box<T> = { value: T }; const n: Box<number> = { value: 42 }; - Build conditional types with the ternary-style syntax for advanced logic.
type IsString<T> = T extends string ? true : false; - Use mapped types to transform every key in another type.
type Optional<T> = { [K in keyof T]?: T[K] }; - Combine aliases with utility types to build reusable transformation patterns.
- Export type aliases from a shared file to keep types organized.
TypeScript Type Aliases
(FAQ)FAQ
Use a type alias when you need unions, intersections, or primitive types — things interfaces can't express. Prefer interfaces for object shapes that other types might extend or implement, since interfaces support declaration merging and are more idiomatic for OOP patterns.
You can't use the extends keyword directly on a type alias, but you can compose them using intersection types: type Admin = User & { role: 'admin' }. For true inheritance hierarchies, interfaces are more ergonomic since they natively support extends.
Declare it with the type keyword and separate each member with a pipe: type Status = 'active' | 'inactive' | 'pending'. You can union primitives, literals, or other type aliases together in a single declaration.
Unlike interfaces, type aliases do not support declaration merging — defining the same type name twice is a compiler error. Rename one of them or consolidate the definitions into a single type alias.
A union type (A | B) means a value can be either A or B, while an intersection type (A & B) means the value must satisfy both A and B simultaneously. Intersections are commonly used to merge object shapes, whereas unions model values that can take one of several forms.
TypeScript Async Patterns
A quick reference for async/await, Promises, error handling, and concurrent operations in TypeScript.
TL;DR
- 01Use async/await to write asynchronous code that reads synchronously.
- 02Always wrap async calls in try/catch to handle Promise rejections.
- 03Use Promise.all to run independent async operations in parallel.
Tips
- 01Use Promise.all for independent operations and sequential awaits for dependent ones — it makes a huge performance difference.
Warnings
- 01Always handle errors in async functions — unhandled rejections can crash your application.
TypeScript Async Patterns
(continued)Async/Await Basics
- Use async/await for readable asynchronous code.
async function fetchUser(id: number): Promise<User> { const response = await fetch(`/api/users/${id}`); return response.json(); } - Await pauses execution until the Promise resolves.
async function loadData() { const data = await fetchUser(1); // waits here console.log(data.name); // runs after fetch completes } - Async functions always return a Promise automatically.
async function getValue(): Promise<number> { return 42; // wrapped in Promise automatically } - Chain multiple awaits for sequential dependent calls.
async function loadProfile(userId: number) { const user = await fetchUser(userId); const posts = await fetchPosts(user.id); return { user, posts }; } - Mark top-level module code as async when needed.
// top-level await in modules const config = await loadConfig(); console.log(config.apiUrl);
TypeScript Async Patterns
(continued)Error Handling
- Handle errors with try/catch blocks.
async function fetchData() { try { const data = await fetch('/api/data'); return data.json(); } catch (error) { console.error('Failed to fetch:', error); return null; } } - Catch both fetch and JSON parsing errors in one block.
async function safeLoad(url: string) { try { const res = await fetch(url); const json = await res.json(); // can also throw return json; } catch (err) { return null; } } - Use a typed catch block to inspect the error shape.
async function fetchUser(id: number) { try { return await getUser(id); } catch (err) { if (err instanceof Error) { console.error(err.message); } } } - Re-throw after logging to propagate to the caller.
async function loadConfig() { try { return await fetchConfig(); } catch (err) { logger.error('Config load failed', err); throw err; // let the caller decide } } - Handle HTTP errors separately from network errors.
async function apiGet(url: string) { const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return res.json(); }
TypeScript Async Patterns
(continued)Concurrent Operations
- Run multiple async operations in parallel with Promise.all.
async function loadAllData() { const [users, posts, comments] = await Promise.all([ fetchUsers(), fetchPosts(), fetchComments() ]); return { users, posts, comments }; } - Promise.all is faster than sequential awaits for independent calls.
// Slow: sequential (waits each time) const user = await fetchUser(1); const posts = await fetchPosts(1); // Fast: parallel (runs at same time) const [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]); - Use Promise.allSettled to collect all results even on failure.
const results = await Promise.allSettled([ fetchUser(1), fetchUser(2), fetchUser(3) ]); results.forEach(r => { if (r.status === "fulfilled") console.log(r.value); else console.error(r.reason); }); - Use Promise.race to get the first resolved result.
const fastest = await Promise.race([ fetchFromRegion("us"), fetchFromRegion("eu") ]); - Use Promise.any to get first fulfilled (ignores rejections).
const first = await Promise.any([ fetchMirror1(), fetchMirror2(), fetchMirror3() ]);
TypeScript Async Patterns
(continued)Sequential Operations
- Run operations in sequence when the result depends on the previous call.
async function getPostWithAuthor(postId: number) { const post = await fetchPost(postId); const author = await fetchUser(post.userId); return { post, author }; } - Await each operation in order to preserve dependency.
async function createAndNotify(data: NewUser) { const user = await createUser(data); const token = await generateToken(user.id); await sendWelcomeEmail(user.email, token); return user; } - Loop with await to process items one at a time.
async function processQueue(ids: number[]) { for (const id of ids) { await processItem(id); // each waits before next } } - Use reduce to accumulate sequential async results.
async function runSteps(steps: Array<() => Promise<string>>) { return steps.reduce(async (prev, step) => { const results = await prev; const result = await step(); return [...results, result]; }, Promise.resolve<string[]>([])); } - Use for-await-of to iterate async iterables.
async function readStream(stream: AsyncIterable<string>) { for await (const chunk of stream) { console.log(chunk); } }
TypeScript Async Patterns
(continued)Type-Safe Async Operations
- Type Promise return values explicitly for caller safety.
async function getData(): Promise<{ id: number; name: string }> { const response = await fetch('/api/data'); return response.json(); } - Use generics to build reusable typed fetch helpers.
async function fetchData<T>(url: string): Promise<T> { const response = await fetch(url); return response.json() as T; } const user = await fetchData<User>('/api/user/1'); - Type async callbacks passed as arguments.
async function withRetry<T>( fn: () => Promise<T>, retries: number ): Promise<T> { for (let i = 0; i <= retries; i++) { try { return await fn(); } catch (e) { if (i === retries) throw e; } } throw new Error("unreachable"); } - Use Result types to encode success and failure explicitly.
type Result<T> = { ok: true; value: T } | { ok: false; error: string }; async function safeFetch(url: string): Promise<Result<unknown>> { try { const data = await (await fetch(url)).json(); return { ok: true, value: data }; } catch (e) { return { ok: false, error: String(e) }; } } - Annotate async generator functions with their yield types.
async function* paginate<T>( load: (page: number) => Promise<T[]> ): AsyncGenerator<T> { let page = 0; while (true) { const items = await load(page++); if (!items.length) break; for (const item of items) yield item; } }
TypeScript Async Patterns
(FAQ)FAQ
Annotate the return type as Promise
Sequential awaits run one after another, so total time is the sum of each duration. Promise.all launches all operations simultaneously and resolves when the last one finishes, which is far faster when the operations don't depend on each other's results.
It does catch them — but only if you await Promise.all inside the try block. If you store the Promise without awaiting it, the rejection goes unhandled. Always use await Promise.all([...]) directly inside try/catch.
TypeScript types caught errors as unknown, so narrow the type before using it: if (error instanceof Error) { console.log(error.message) }. Avoid casting to any, as that defeats type safety.
Technically yes, but the loop won't wait for each async call to finish — forEach doesn't await the returned Promises. Use a for...of loop with await instead, or Promise.all with .map() if the iterations are independent.
TypeScript Declaration Files
Write .d.ts files, publish typed npm packages, and create type stubs.
TL;DR
- 01Create .d.ts files to provide types for JavaScript libraries.
- 02Use declare keyword to define types without implementation.
- 03Publish types alongside your npm package for TypeScript users.
Tips
- 01Always include declaration files with your npm package or publish to DefinitelyTyped — TypeScript users will appreciate the type safety.
Warnings
- 01Keep declaration files accurate and in sync with implementation — incorrect types are worse than no types at all.
TypeScript Declaration Files
(continued)Writing Declaration Files
- Create .d.ts files to define types for JavaScript code.
// mylib.d.ts export function greet(name: string): string; export interface User { id: number; name: string; } - Declaration files contain only type definitions, no implementation.
// index.d.ts export class Calculator { add(a: number, b: number): number; subtract(a: number, b: number): number; } - Use declare to define types for existing JavaScript globals.
declare global { interface Window { myGlobal: string; } } - Declare ambient variables that exist at runtime but not in source.
declare const __VERSION__: string; declare function require(module: string): any; - Use declare module for UMD or non-ES library entry points.
export as namespace MyLib; export function init(options: Options): void; export interface Options { debug?: boolean; }
TypeScript Declaration Files
(continued)Module Declaration
- Declare types for entire modules or namespaces.
declare module "my-library" { export function process(data: any): any; export const version: string; } - Augment existing modules with additional types.
declare module "express" { interface Request { userId?: number; } } - Use triple-slash directives to reference other declaration files.
/// <reference path="./types.d.ts" /> /// <reference types="node" /> - Organize types with namespaces for large libraries.
declare namespace MyLib { interface Config { } function init(config: Config): void; } - Use a wildcard module declaration for file imports like CSS or SVG.
declare module "*.svg" { const content: string; export default content; } declare module "*.css" { const styles: Record<string, string>; export default styles; }
TypeScript Declaration Files
(continued)Package Configuration
- Point to declaration files in package.json with the types field.
{ "name": "my-library", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts" } - Emit declaration files from TypeScript during build.
{ "compilerOptions": { "declaration": true, "declarationDir": "./dist", "outDir": "./dist" } } - Use typesVersions to support different TypeScript versions.
{ "typesVersions": { "<=4.0": { "*": ["ts4.0/*"] }, ">=4.1": { "*": ["*"] } } } - Use exports map with types conditions for dual CJS/ESM packages.
{ "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.ts", "default": "./dist/cjs/index.js" } } } } - Generate declaration maps for go-to-definition in source.
{ "compilerOptions": { "declarationMap": true, "declaration": true, "sourceRoot": "./src" } }
TypeScript Declaration Files
(continued)Publishing to DefinitelyTyped
- Contribute type definitions for popular untyped libraries.
DefinitelyTyped/types/ library-name/ index.d.ts package.json tsconfig.json tests.ts - Test declaration files with sample code before submitting.
// tests.ts import * as lib from "./index"; const result: string = lib.process("test"); const version: string = lib.version; - Include a package.json in the types folder with the correct shape.
{ "name": "@types/library-name", "version": "1.0.0", "typings": "index.d.ts" } - Add a tsconfig.json to validate declarations during CI.
{ "compilerOptions": { "module": "commonjs", "lib": ["es6"], "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true } } - Run dtslint to catch type errors before opening a pull request.
npm install -g dtslint dtslint types/library-name
TypeScript Declaration Files
(continued)Best Practices
- Keep declaration files in sync with implementation.
// Don't declare types that don't match the implementation export function add(a: number, b: number): number; // Should match: const add = (a, b) => a + b; - Export only public APIs and hide internal helpers.
// Correct: public types are exported export interface PublicAPI { } // Wrong: internal types shouldn't be exported // export interface _InternalHelper { } - Include JSDoc comments for better IDE support.
/** * Processes data asynchronously * @param data - The input data to process * @returns A promise that resolves with the result */ export function processAsync(data: any): Promise<any>; - Use semver versioning for types to avoid breaking changes.
# Bump patch for additions, minor for new overloads, major for removals npm version patch # safe: new optional param npm version major # breaking: removed export - Avoid using any in declaration files — prefer unknown or generics.
// Weak: loses type information export function parse(input: string): any; // Better: let the caller specify the return type export function parse<T>(input: string): T;
TypeScript Declaration Files
(FAQ)FAQ
A .d.ts file contains only type declarations with no runtime code — it uses the declare keyword to describe the shape of existing JavaScript without any implementation. Regular .ts files compile to JavaScript, while declaration files are consumed only by the TypeScript compiler for type checking.
Add a "types" or "typings" field in your package.json pointing to your main .d.ts file, e.g. "types": "./dist/index.d.ts". If your types file has the same name as your "main" entry but with a .d.ts extension, TypeScript will find it automatically without the explicit field.
Publish to DefinitelyTyped (@types/yourpackage) when you're adding types for a third-party JavaScript library you don't own or maintain. If you own the package, bundle the .d.ts files directly — it keeps types versioned in sync with the implementation and simplifies the install process for consumers.
Use export = myFunction syntax in your declaration file instead of export default, since export default doesn't match how CommonJS modules work at runtime. Consumers using import myFn from 'pkg' will need esModuleInterop: true in their tsconfig to use export = style declarations.
Yes — set "declaration": true in your tsconfig.json and TypeScript will emit .d.ts files alongside your compiled output automatically. This is the recommended approach for TypeScript-authored libraries; hand-written declarations are only necessary when typing an existing JavaScript codebase.
TypeScript Generics
Master generics to build reusable, type-safe functions, classes, and components in TypeScript.
TL;DR
- 01Use type parameters to write reusable, type-safe code.
- 02Constrain generics to restrict what types they accept.
- 03Provide default types to simplify common generic usage.
Tips
- 01Name generics meaningfully, like <code>TItem</code> or <code>TResponse</code>, when single letters make the code harder to read.
Warnings
- 01Avoid overusing generics in simple functions, since they can make code harder to read and maintain.
TypeScript Generics
(continued)Basics
- Add a type parameter in angle brackets to make a function generic.
function identity<T>(value: T): T { return value; } - The letter
Tis a placeholder that gets replaced with a real type. - TypeScript infers the type from the argument you pass to the function.
identity("hello"); // T inferred as string identity(42); // T inferred as number - You can also pass the type explicitly using angle brackets at the call.
identity<string>("hello"); - Use single uppercase letters or short descriptive names for type parameters.
TypeScript Generics
(continued)Generic Functions
- Write a generic function once and reuse it for many different types.
function first<T>(items: T[]): T | undefined { return items[0]; } - Return values keep their original type, unlike using
anyfor parameters. - Pass arrays generically with
T[]orArray<T>for typed list operations. - Use multiple type parameters when handling pairs of types.
function pair<T, U>(a: T, b: U): [T, U] { return [a, b]; } - Chain generics through helper functions to keep type information intact.
TypeScript Generics
(continued)Constraints
- Limit a type parameter using
extendsto require certain properties.function byId<T extends { id: number }>(items: T[], id: number) { return items.find(i => i.id === id); } - Constraints make sure the type has the properties your function needs.
- Use
keyof Tto constrain a parameter to keys of another type.function getValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } - Combine constraints with conditional types for advanced type narrowing.
- Constraints still allow types that include extra properties beyond the requirement.
TypeScript Generics
(continued)Default Types
- Set a default type to simplify common use cases for callers.
interface ApiResponse<T = unknown> { data: T; status: number; } - Default types let callers skip the type argument when the default fits.
const res: ApiResponse = { data: null, status: 200 }; - Combine defaults with constraints to keep types both safe and easy.
type Config<T extends object = {}> = { values: T }; - Use defaults to keep public APIs clean while supporting custom overrides.
- Defaults are especially useful for generic React components and utility types.
TypeScript Generics
(continued)Generic Classes
- Add type parameters to class names for typed instances and methods.
class Box<T> { constructor(public value: T) {} get() { return this.value; } } - Use the type parameter inside properties, methods, and constructor signatures.
- Pass the type when creating an instance, or let it be inferred.
const a = new Box<string>("hello"); const b = new Box(42); // inferred as Box<number> - Generic classes work well for collections, queues, stacks, and state stores.
- TypeScript will often infer the type from the constructor argument automatically.
TypeScript Generics
(FAQ)FAQ
Use the extends keyword to restrict what types are accepted, e.g., function getLength<T extends { length: number }>(arg: T): number. This lets TypeScript guarantee properties exist on the type parameter without widening to any.
Use generics when the relationship between input and output types matters — function first<T>(arr: T[]): T preserves that the return type matches the array element type, which any would lose. Union types are better when a fixed set of known types is acceptable and reusability across unknown types isn't needed.
TypeScript infers type parameters from call-site arguments in most cases, so wrap(42) on function wrap<T>(val: T): T[] resolves T as number without annotation. Explicit type arguments like wrap<number>(42) are only needed when inference fails or you want to enforce a stricter type than what would be inferred.
Assign a default with =, like type Result<T = string> = { data: T; error: string | null }, so callers can omit the type argument when the default covers their case. This is particularly useful for generic utility types where one type dominates most usage but the generic remains available for edge cases.
Declare the type parameter on the class itself — class Repository<T> { findById(id: number): T {} } — so every method and property in the class operates on the same T. This differs from a generic method, where T is only in scope for that single method call.
TypeScript Generics Constraints
Build flexible but safe types using constrained generics.
TL;DR
- 01Use extends to constrain generic types to specific shapes.
- 02Use keyof to safely access object properties.
- 03Combine constraints with conditional types for powerful patterns.
Tips
- 01Use constraints to catch errors at compile time instead of runtime — this prevents many subtle bugs in generic code.
Warnings
- 01Over-constraining generics can make types too restrictive — find the right balance between flexibility and safety.
TypeScript Generics Constraints
(continued)Basic Constraints
- Constrain generic type to a specific type with extends.
function getLength<T extends { length: number }>(item: T): number { return item.length; } getLength("hello"); // works: string has length getLength([1, 2, 3]); // works: array has length getLength(123); // error: number has no length - Constrain to primitive types.
function identity<T extends string | number>(value: T): T { return value; } identity("text"); // works identity(42); // works identity({}); // error - Extend specific classes or interfaces.
interface HasId { id: string; } function getId<T extends HasId>(item: T): string { return item.id; }
TypeScript Generics Constraints
(continued)Keyof Constraints
- Use keyof to safely access object properties.
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"); // works: "name" is a key getProperty(user, "email"); // error: "email" is not a key - Create flexible getters with key constraints.
function getProp<T extends object, K extends keyof T>( obj: T, key: K ): T[K] { return obj[key]; }
TypeScript Generics Constraints
(continued)Multiple Constraints
- Combine constraints with intersection types.
interface HasId { id: string; } interface HasName { name: string; } function process<T extends HasId & HasName>(item: T): string { return `${item.id}: ${item.name}`; } - Constrain multiple generic parameters.
function merge<T extends object, U extends object>( obj1: T, obj2: U ): T & U { return { ...obj1, ...obj2 }; } - Use conditional constraints for complex logic.
function getValue<T extends string | number | boolean>( value: T ): T extends string ? string : T extends number ? number : boolean { return value; }
TypeScript Generics Constraints
(continued)Constructor Constraints
- Constrain generic to a class constructor.
function create<T extends { new (...args: any[]): {} }>( constructor: T ): InstanceType<T> { return new constructor(); } class User { } const user = create(User); // user is typed as User - Accept class instances with type preservation.
function instantiate<T extends new () => any>( Class: T ): InstanceType<T> { return new Class(); }
TypeScript Generics Constraints
(continued)Practical Patterns
- Create type-safe array utilities with constraints.
function findById<T extends { id: string | number }>( items: T[], id: string | number ): T | undefined { return items.find(item => item.id === id); } - Build flexible mappers with key constraints.
function mapProperty<T, K extends keyof T>( items: T[], key: K ): T[K][] { return items.map(item => item[key]); } const names = mapProperty(users, "name"); - Create type-safe event handlers.
type EventMap = { login: { userId: string }; logout: { timestamp: number }; }; function on<E extends keyof EventMap>( event: E, handler: (data: EventMap[E]) => void ) { // Type-safe handler }
TypeScript Generics Constraints
(FAQ)FAQ
Use extends with an object shape: function getLength<T extends { length: number }>(arg: T) ensures T always has a length property. This catches type errors at compile time rather than failing at runtime.
keyof T produces a union of all keys of T, while T extends keyof U constrains T to be one of U's keys. Use the latter to safely index into objects: function get<T, K extends keyof T>(obj: T, key: K): T[K].
Yes, use an intersection type: function process<T extends Serializable & Identifiable>(item: T) requires T to satisfy both shapes simultaneously. This is more flexible than creating a combined interface when you don't control the types.
Constrain with new(): function create<T>(ctor: new () => T): T { return new ctor(); }. For constructors with arguments, use new (...args: any[]) => T to accept any constructor signature.
Constraints only guarantee the minimum shape — TypeScript still won't let you access properties not declared in the constraint, even if they exist at runtime. Narrow the constraint to include every property you access, or use a type assertion as a last resort inside the function body.
TypeScript Null Safety
A reference for handling null and undefined safely in TypeScript projects.
TL;DR
- 01Use optional chaining (?.) to safely access nested properties on nullable values.
- 02Use nullish coalescing (??) to supply default values for null or undefined.
- 03Apply type guards to narrow a union type before accessing its properties.
Tips
- 01Enable strictNullChecks in tsconfig.json to catch null safety issues at compile time.
Warnings
- 01Non-null assertions (!) bypass type checking — use them only when you're certain the value is not null.
TypeScript Null Safety
(continued)Optional Chaining
- Safely access nested properties with optional chaining.
const user: User | null = getUser(); // Without optional chaining (error if user is null) const name = user.profile.name; // Error // With optional chaining (safe) const name = user?.profile?.name; // undefined if user is null - Optional chaining returns undefined if any part is null or undefined.
const city = user?.address?.city; // undefined rather than throwing - Chain optional method calls with ?. before the parentheses.
const length = user?.getName?.(); // undefined if getName doesn't exist - Combine optional chaining with nullish coalescing for fallback values.
const displayName = user?.profile?.displayName ?? "Anonymous"; - Use optional chaining on array access and computed properties.
const firstTag = post?.tags?.[0]; // undefined if tags is absent const value = map?.["dynamic-key"]?.trim(); // safe dynamic key access
TypeScript Null Safety
(continued)Nullish Coalescing
- Use ?? to provide default values only for null or undefined.
const name = user?.name ?? "Guest"; const count = value ?? 0; - Unlike ||, ?? does not replace empty strings or zero.
const count = 0; console.log(count || 10); // 10 — wrong, zero is falsy console.log(count ?? 10); // 0 — correct, zero is not null - Use ??= to assign a default only when the variable is null or undefined.
let config: Config | null = null; config ??= defaultConfig; // assigned only when null or undefined - Chain ?? with optional chaining for safe nested default values.
const role = user?.permissions?.role ?? "viewer"; - Use ?? in function parameters to handle missing arguments.
function paginate(page: number | null, size: number | null) { const p = page ?? 1; const s = size ?? 20; return { page: p, size: s }; }
TypeScript Null Safety
(continued)Type Guards
- Check for null before accessing properties.
function printLength(value: string | null) { if (value !== null) { console.log(value.length); // Safe: value is string } } - Use typeof to guard primitive types.
if (typeof value === "string") { console.log(value.toUpperCase()); // Safe: narrowed to string } - Use instanceof to narrow class instances.
function handle(err: unknown) { if (err instanceof Error) { console.error(err.message); // Safe: narrowed to Error } } - Write a custom type guard function with a type predicate.
function isUser(value: unknown): value is User { return typeof value === "object" && value !== null && "name" in value; } if (isUser(data)) { console.log(data.name); // Safe: narrowed to User } - Use in operator to narrow discriminated union types.
type Cat = { meow(): void }; type Dog = { bark(): void }; function speak(animal: Cat | Dog) { if ("meow" in animal) animal.meow(); else animal.bark(); }
TypeScript Null Safety
(continued)Optional Types
- Mark properties as optional with ? to allow undefined.
interface User { name: string; email?: string; // can be undefined phone: string | null; // can be null (must be explicit) } - Optional properties don't need to be included when constructing an object.
const user: User = { name: "Alice" // email is optional — safe to omit }; - Distinguish optional (?) from nullable (| null) for precise types.
interface Post { title: string; subtitle?: string; // missing or undefined deletedAt: Date | null; // always present, but can be null } - Mark function parameters optional to make them skippable.
function greet(name: string, title?: string): string { return title ? `Hello, ${title} ${name}` : `Hello, ${name}`; } greet("Alice"); // OK greet("Alice", "Dr."); // OK - Use Required
to remove optional modifiers when needed. interface Options { timeout?: number; retries?: number; } function runWithDefaults(opts: Required<Options>) { console.log(opts.timeout, opts.retries); // both guaranteed present }
TypeScript Null Safety
(continued)Non-Null Assertion
- Use ! to tell TypeScript a value is not null or undefined.
const value = getValue(); const length = value!.length; // Assert value is not null - Prefer an explicit null check over the non-null assertion.
// Good: check first — safe at runtime if (value !== null) { console.log(value.length); } // Avoid: assertion without check — can throw at runtime console.log(value!.length); - Use ! on DOM queries when you know the element exists.
const input = document.getElementById("email")!; // Safe only if the element is guaranteed in the HTML - Avoid ! inside library code — callers may not share your assumptions.
// Better: surface the possibility of null in the return type function findUser(id: number): User | null { return db.users.find(u => u.id === id) ?? null; } - Use optional chaining as a safer alternative to non-null assertion.
// Assertion — crashes if null element!.classList.add("active"); // Optional chaining — silently skips if null element?.classList.add("active");
TypeScript Null Safety
(FAQ)FAQ
Optional chaining (?.) short-circuits to undefined when it encounters null or undefined at any point in the chain, while && short-circuits to the falsy value itself (including 0 or empty string). Prefer ?. when you only care about null/undefined, and && when you need to guard against all falsy values.
Use ?? when your fallback should only trigger for null or undefined, not for other falsy values like 0, false, or empty string. For example, count ?? 0 keeps 0 as a valid value, whereas count || 0 would incorrectly replace it.
Use a type guard such as if (value !== null && value !== undefined) or if (value) before accessing properties — TypeScript will narrow the type inside the block. You can also write a custom type guard function with a value is Type return annotation for reusable narrowing logic.
An optional property (name?: string) can be omitted from the object entirely and has an implicit undefined, while an explicit union (name: string | null) requires the property to be present but allows null as a value. Choose optional for properties that may not exist, and string | null when the property must always be declared but can have no value.
This usually happens when the value is accessed outside the narrowing block, reassigned between the check and use, or when TypeScript can't track the control flow — for instance, after an async gap or inside a callback. Move the property access inside the guard block, or assign the narrowed value to a const before the async operation.
TypeScript Strict Mode
Understand strict mode benefits, gotchas, and how to enable it progressively.
TL;DR
- 01Enable "strict": true in tsconfig.json for maximum type safety.
- 02Strict mode includes multiple flags that can be enabled individually.
- 03Migrate existing code gradually by enabling flags one at a time.
Tips
- 01Enable strict mode from the start on new projects — fixing types upfront is easier than retrofitting them later.
Warnings
- 01Enabling strict mode on large existing codebases requires significant effort — plan for gradual migration over multiple sprints.
TypeScript Strict Mode
(continued)Enabling Strict Mode
- Enable strict mode to enable all strict flags at once.
{ "compilerOptions": { "strict": true } } - Strict mode enables these flags simultaneously.
{ "compilerOptions": { "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true } } - New TypeScript projects should use strict mode from the start.
{ "compilerOptions": { "strict": true, "target": "ES2020", "module": "ESNext" } } - Existing projects can migrate gradually by enabling individual flags.
{ "compilerOptions": { "strict": false, "noImplicitAny": true } } - Use noEmitOnError to prevent emitting code when type errors exist.
{ "compilerOptions": { "strict": true, "noEmitOnError": true } }
TypeScript Strict Mode
(continued)Implicit Any Errors
- Require types for variables without explicit type annotations.
// With noImplicitAny: error function add(a, b) { return a + b; } // Fixed: explicit types function add(a: number, b: number): number { return a + b; } - Enable noImplicitAny to catch typing mistakes early.
// Error: parameter 'event' implicitly has an 'any' type document.addEventListener("click", (event) => { }); // Fixed document.addEventListener("click", (event: MouseEvent) => { }); - Annotate callback parameters that TypeScript cannot infer.
// Error: 'item' implicitly has type 'any' const items = JSON.parse(data); items.forEach((item) => console.log(item)); // Fixed: type the parsed value const items: string[] = JSON.parse(data); items.forEach((item: string) => console.log(item)); - Use unknown instead of any when the type is genuinely unknown.
function process(input: unknown) { if (typeof input === "string") { console.log(input.toUpperCase()); // Safe after narrowing } } - Reduces bugs from unintended type mismatches throughout the codebase.
// Without noImplicitAny: silent runtime bug function double(n) { return n * 2; } double("3"); // "33" at runtime — no compile-time error
TypeScript Strict Mode
(continued)Null Check Errors
- Treat null and undefined as distinct types.
// With strictNullChecks: error const name: string = null; // Fixed: include null in type const name: string | null = null; - Require null checks before accessing properties.
function getName(user: User | null) { if (!user) return "Guest"; return user.name; // Safe: user is not null here } - Prevents many common runtime errors from unchecked nulls.
// Error: Object is possibly undefined const first = items[0].name; // Fixed const first = items[0]?.name ?? "Unknown"; - Enable strictNullChecks independently before enabling full strict mode.
{ "compilerOptions": { "strictNullChecks": true } } - Most impactful strict flag — catching nullable bugs early saves debugging time.
// Without strictNullChecks: no error, crashes at runtime const user = getUser(); // may return null console.log(user.name); // TypeError: Cannot read properties of null
TypeScript Strict Mode
(continued)Property Init Errors
- Require class properties to be initialized in the constructor.
// With strictPropertyInitialization: error class User { name: string; } // Fixed: initialize in constructor class User { name: string; constructor(name: string) { this.name = name; } } - Use definite assignment assertion (!) if initialization is deferred.
class User { name!: string; // tells TypeScript: trust me, this will be set async init() { this.name = await fetchName(); } } - Initialize properties with a default value to satisfy the compiler.
class Config { retries: number = 3; timeout: number = 5000; debug: boolean = false; } - Use readonly with constructor initialization for immutable properties.
class Product { readonly id: string; readonly name: string; constructor(id: string, name: string) { this.id = id; this.name = name; } } - Mark properties that are set outside the constructor with declare if needed.
class Widget { declare element: HTMLElement; // set by framework before use render() { this.element.innerHTML = "Hello"; } }
TypeScript Strict Mode
(continued)Progressive Migration
- Disable strict mode initially for large migrations.
{ "compilerOptions": { "strict": false } } - Enable individual flags gradually as code is typed.
{ "compilerOptions": { "strict": false, "noImplicitAny": true } } - Use stricter settings for new code while migrating old code.
{ "compilerOptions": { "strict": false }, "ts-node": { "compilerOptions": { "strict": true } } } - Gradually increase strictness until full strict mode is enabled.
{ "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true, "strictPropertyInitialization": true, "strict": true } } - Use @ts-strict-ignore comments to suppress errors in files not yet migrated.
// @ts-strict-ignore // This file is pending strict migration — tracked in issue #42 export function legacyHelper(data) { return data.value; }
TypeScript Strict Mode
(FAQ)FAQ
It enables a bundle of flags including strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, and others. You can enable each flag individually if you want finer control during migration.
Use optional chaining (obj?.prop), nullish coalescing (val ?? default), or explicit null checks (if (val !== null)) before accessing the value. Type narrowing with typeof or instanceof also works for more complex cases.
Not directly per-file, but you can use multiple tsconfig.json files — a strict one for new code and a looser one for legacy code — by splitting source directories and using project references.
noImplicitAny is one of several flags that strict mode bundles together; enabling strict: true is equivalent to enabling noImplicitAny plus strictNullChecks, strictFunctionTypes, strictBindCallApply, and strictPropertyInitialization all at once.
strictPropertyInitialization requires that class properties are either initialized in the declaration or definitely assigned in the constructor. Use a definite assignment assertion (prop!: Type) if you know it will be set externally, or initialize it to a default value.
TypeScript Type Narrowing
Master typeof, instanceof, in, and discriminated unions to safely work with union types.
TL;DR
- 01Narrow union types using typeof, instanceof, and in checks.
- 02Use discriminated unions for safer object type narrowing.
- 03Build custom type guards with the is keyword.
Tips
- 01Reach for discriminated unions when modeling state, since they pair perfectly with switch statements and exhaustive narrowing.
Warnings
- 01A failing assertion function throws at runtime, so always wrap calls in try/catch when the input is untrusted.
TypeScript Type Narrowing
(continued)Typeof Checks
- Use
typeofto narrow primitive types like string, number, and boolean.function format(value: string | number) { if (typeof value === "string") { return value.trim(); } return value.toFixed(2); } - TypeScript automatically narrows the type inside each branch.
- Works for
string,number,boolean,bigint,symbol,undefined, andfunction. - This is the simplest form of narrowing and works with most union types.
- Use it whenever a union mixes primitive types together.
TypeScript Type Narrowing
(continued)Instanceof and Truthiness
- Use
instanceofto narrow object types created from classes.if (error instanceof Error) { console.log(error.message); } - Check for
nullorundefinedto narrow nullable union types.function greet(name: string | null) { if (name) return `Hi ${name}`; return "Hi friend"; } - Truthiness checks narrow out falsy values like null, undefined, and empty string.
- Use optional chaining
?.alongside narrowing for safer property access. - Combine with nullish coalescing
??to provide default values.
TypeScript Type Narrowing
(continued)The In Operator
- Use
into check if a property exists on an object.type Dog = { bark(): void }; type Cat = { meow(): void }; function speak(pet: Dog | Cat) { if ("bark" in pet) pet.bark(); else pet.meow(); } - TypeScript narrows the type based on which property is present.
- Useful when union members have different shapes without a common tag.
- Works for both own properties and inherited ones on the object.
- Pair with discriminated unions for even cleaner narrowing.
TypeScript Type Narrowing
(continued)Discriminated Unions
- Add a shared literal property to union members for safe narrowing.
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number }; function area(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius ** 2; } return shape.side ** 2; } - The shared
kindproperty acts as a discriminator for narrowing. - TypeScript automatically narrows to the matching member in each branch.
- Pair with a
switchstatement for clean handling of every case. - This is the most reliable pattern for narrowing complex unions.
TypeScript Type Narrowing
(continued)Custom Type Guards
- Write a function that returns a type predicate using the
iskeyword.function isString(value: unknown): value is string { return typeof value === "string"; } - The compiler uses the predicate to narrow types in calling code.
if (isString(value)) { console.log(value.toUpperCase()); } - Use type guards for complex checks that built-in operators cannot express.
- Use
asserts value is Typefor guards that throw on failure.function assertString(v: unknown): asserts v is string { if (typeof v !== "string") throw new Error("Not a string"); } - Custom guards keep narrowing logic reusable across your codebase.
TypeScript Type Narrowing
(FAQ)FAQ
Use a typeof check: if (typeof value === 'string') { ... }. Inside that block TypeScript automatically narrows value to string, giving you full type-safe access to string methods.
Use discriminated unions whenever a variable can represent multiple distinct states—for example, a result that is either { status: 'ok', data: T } or { status: 'error', message: string }. The shared literal field lets TypeScript narrow exhaustively in a switch statement, catching unhandled cases at compile time.
instanceof checks the prototype chain and works for class instances (e.g., if (err instanceof Error)). The in operator checks for property existence and is the right choice for plain objects and interfaces, since interfaces have no runtime constructor to test against.
Define a function with a return type of the form value is SomeType: function isCat(animal: Animal): animal is Cat { return (animal as Cat).purr !== undefined; }. When this function returns true, TypeScript narrows the argument to Cat in the calling scope.
Assertion functions (those that assert value is T) throw an error rather than returning false, so unhandled exceptions will crash your program. Guard any assertion call on untrusted data—like API responses—with a try/catch block, or use a boolean type guard instead when the input may legitimately not match.
TypeScript With React
Type React components, props, and hooks safely with TypeScript.
TL;DR
- 01Define prop interfaces or types for component APIs.
- 02Use React.FC<Props> or function return types for components.
- 03Extend built-in React types for events and refs.
Tips
- 01Export prop interfaces so consumers can extend or override them when needed for customization.
Warnings
- 01Avoid using the any type — specific types catch bugs at compile time and make refactoring safer.
TypeScript With React
(continued)Typing Component Props
Define props with an interface — use ? for optional props with defaults.
interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; } function Button({ label, onClick, disabled = false }: ButtonProps) { return <button onClick={onClick} disabled={disabled}>{label}</button>; }Use type instead of interface when you need union or intersection props.
type CardProps = { title: string; variant: "primary" | "secondary"; }; function Card({ title, variant }: CardProps) { return <div className={variant}><h2>{title}</h2></div>; }Export prop interfaces so consumers can extend or build on them.
export interface ButtonProps { label: string; onClick: () => void; } export function Button(props: ButtonProps) { /* ... */ }Extend HTML element attributes to allow all native props on wrappers.
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { label: string; } function LabeledInput({ label, ...rest }: InputProps) { return <label>{label}<input {...rest} /></label>; }Use React.ComponentPropsWithoutRef to forward all props of a native tag.
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> { variant: "primary" | "ghost"; } function Button({ variant, ...rest }: ButtonProps) { return <button className={variant} {...rest} />; }
TypeScript With React
(continued)Event Handlers
Type a change handler for text inputs using React.ChangeEvent.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setName(e.target.value); // e.target.value is string };Type a form submit handler using React.FormEventHandler.
function Form() { const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { e.preventDefault(); // handle form data }; return <form onSubmit={handleSubmit}></form>; }Type a mouse click handler using React.MouseEventHandler.
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => { console.log(e.button); // 0 = left, 1 = middle, 2 = right };Type a keyboard handler to respond to specific key presses.
function SearchBox() { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") submitSearch(); }; return <input onKeyDown={handleKeyDown} />; }Define typed handler props in an interface for reusable form components.
interface TableRowProps { onRowClick: (id: number, e: React.MouseEvent<HTMLTableRowElement>) => void; } function TableRow({ onRowClick }: TableRowProps) { return <tr onClick={(e) => onRowClick(1, e)}></tr>; }
TypeScript With React
(continued)Typing Hooks
Pass a union type to useState when the initial value can be null.
const [count, setCount] = useState<number>(0); const [user, setUser] = useState<User | null>(null);Type useRef for DOM elements and access it with optional chaining.
const inputRef = useRef<HTMLInputElement>(null); function focus() { inputRef.current?.focus(); // safe — current may be null }Type createContext with a default and wrap it in a typed custom hook.
const UserContext = React.createContext<User | undefined>(undefined); function useUser() { const context = useContext(UserContext); if (!context) throw new Error("useUser needs provider"); return context; }Type useReducer with a discriminated union to cover every action shape.
type Action = | { type: "INCREMENT"; payload: number } | { type: "DECREMENT"; payload: number }; const reducer = (state: number, action: Action) => { switch (action.type) { case "INCREMENT": return state + action.payload; case "DECREMENT": return state - action.payload; } };Use as const on the return array to get a precise tuple type from a custom hook.
function useToggle(initial: boolean) { const [value, setValue] = useState(initial); const toggle = () => setValue(v => !v); return [value, toggle] as const; // [boolean, () => void] }
TypeScript With React
(continued)Children and Refs
Use React.ReactNode for children props — it accepts JSX, strings, or null.
interface WrapperProps { children: React.ReactNode; } function Wrapper({ children }: WrapperProps) { return <div className="wrapper">{children}</div>; }Use React.PropsWithChildren to add children to any existing props type.
interface CardProps { title: string; } function Card({ title, children }: React.PropsWithChildren<CardProps>) { return <div><h2>{title}</h2>{children}</div>; }Use useRef with a DOM type and access .current safely with optional chaining.
const videoRef = useRef<HTMLVideoElement>(null); function play() { videoRef.current?.play(); // optional chain handles null }Use React.forwardRef with explicit generic types for ref-forwarding components.
const TextInput = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( (props, ref) => <input ref={ref} {...props} /> );Distinguish RefObject (read-only current) from MutableRefObject (writable current).
// React.RefObject<T>: current is readonly — for DOM refs via useRef(null) const domRef: React.RefObject<HTMLDivElement> = useRef(null); // React.MutableRefObject<T>: current is writable — for storing values const timerRef: React.MutableRefObject<number> = useRef(0);
TypeScript With React
(continued)TypeScript Config for React
Set jsx to react-jsx to avoid importing React in every file.
{ "compilerOptions": { "jsx": "react-jsx" } }Enable strict mode to activate all strict type checks at once.
{ "compilerOptions": { "strict": true } }Enable noUncheckedIndexedAccess to catch undefined array access at compile time.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } } // items[0] is now string | undefined instead of stringUse paths to create tsconfig aliases for cleaner imports across large projects.
{ "compilerOptions": { "paths": { "@components/*": ["./src/components/*"], "@lib/*": ["./src/lib/*"] } } }Set composite: true to enable project references in monorepo packages.
{ "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true } }
TypeScript With React
(FAQ)FAQ
Use React.ChangeEvent
React.FC implicitly typed children in older React versions and can hide return type mismatches, so many teams prefer plain functions with an explicit return type of JSX.Element or React.ReactElement. Either approach works — pick one and stay consistent across the codebase.
Pass a union type as the generic: useState<User | null>(null). TypeScript will then enforce null checks before you access properties on the state value.
Add a type parameter to the function signature: function List
Extend the corresponding React HTML attribute type: interface ButtonProps extends React.ButtonHTMLAttributes
TypeScript With React Patterns
Common TypeScript patterns for React components, hooks, and props.
TL;DR
- 01Type props with interfaces or types for component APIs.
- 02Use React.FC<Props> or function return types for components.
- 03Extend event types from React for type-safe handlers.
Tips
- 01Use discriminated unions for any component that accepts fundamentally different prop shapes — it removes the need for runtime checks.
Warnings
- 01Avoid over-typing or using any — specific types prevent bugs and make refactoring safer.
TypeScript With React Patterns
(continued)Discriminated Union Props
Define a multi-variant component using a type union with a shared discriminant.
type AlertProps = | { variant: "success"; message: string } | { variant: "error"; message: string; code: number }; function Alert(props: AlertProps) { return <div className={props.variant}>{props.message}</div>; }Access variant-specific props only after narrowing on the discriminant field.
function Alert(props: AlertProps) { if (props.variant === "error") { console.log(props.code); // only available in "error" branch } return <div>{props.message}</div>; }Use an exhaustive switch with never to catch unhandled variants at compile time.
function renderBadge(props: BadgeProps): React.ReactNode { switch (props.variant) { case "count": return <span>{props.count}</span>; case "dot": return <span className="dot" />; default: { const _never: never = props; // compile error if a variant is missing return null; } } }Model a button that accepts either a label or an icon, but not both.
type ButtonProps = | { kind: "label"; label: string } | { kind: "icon"; icon: React.ReactNode; ariaLabel: string }; function Button(props: ButtonProps) { return props.kind === "label" ? <button>{props.label}</button> : <button aria-label={props.ariaLabel}>{props.icon}</button>; }Require a message for error alerts but not for success alerts.
type ToastProps = | { type: "success"; title: string } | { type: "error"; title: string; detail: string }; // Callers must provide detail when type is "error" const t: ToastProps = { type: "error", title: "Failed", detail: "404" };
TypeScript With React Patterns
(continued)Polymorphic Components
Define helper types that extract the props for any HTML element or component.
type AsProps<E extends React.ElementType> = { as?: E; } & React.ComponentPropsWithoutRef<E>;Build a polymorphic Box component that types native props based on the as tag.
function Box<E extends React.ElementType = "div">( { as, ...rest }: AsProps<E> ) { const Tag = as ?? "div"; return <Tag {...rest} />; } // <Box as="a" href="/"> — href is required and typed // <Box as="button" onClick={fn}> — onClick is typed for buttonUse React.ElementType as a prop type for flexible tag or component injection.
interface HeadingProps { as?: React.ElementType; // accepts "h1", "h2", MyComponent, etc. children: React.ReactNode; } function Heading({ as: Tag = "h1", children }: HeadingProps) { return <Tag>{children}</Tag>; }Avoid unsafe casting with as unknown — let generics flow through instead.
// Bad: loses type safety function Box({ as: Tag = "div", ...rest }: any) { return <Tag {...(rest as any)} />; } // Good: generics preserve prop types function Box<E extends React.ElementType = "div">({ as, ...rest }: AsProps<E>) { const Tag = as ?? "div"; return <Tag {...rest} />; }Pass a custom component as the as prop to reuse any component's interface.
function Link({ href, children }: { href: string; children: React.ReactNode }) { return <a href={href}>{children}</a>; } // Box as Link — Box now accepts href because Link does <Box as={Link} href="/home">Home</Box>
TypeScript With React Patterns
(continued)Forward Ref Patterns
Use React.forwardRef with explicit generics to type the ref and props correctly.
const TextInput = React.forwardRef< HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement> >((props, ref) => <input ref={ref} {...props} />);Expose a custom handle interface using useImperativeHandle instead of the DOM ref.
interface DialogHandle { open: () => void; close: () => void; } const Dialog = React.forwardRef<DialogHandle, { children: React.ReactNode }>( ({ children }, ref) => { const [open, setOpen] = useState(false); useImperativeHandle(ref, () => ({ open: () => setOpen(true), close: () => setOpen(false) })); return open ? <div>{children}</div> : null; } );Use the forwarded ref at the call site with useRef typed to the handle.
const dialogRef = useRef<DialogHandle>(null); function Page() { return ( <> <button onClick={() => dialogRef.current?.open()}>Open</button> <Dialog ref={dialogRef}>Hello</Dialog> </> ); }Forward refs in component libraries to let consumers control focus.
const Select = React.forwardRef<HTMLSelectElement, React.SelectHTMLAttributes<HTMLSelectElement>>( (props, ref) => <select ref={ref} {...props} /> ); // Consumers can now do: const ref = useRef<HTMLSelectElement>(null);Assign displayName to forwardRef components for better DevTools labels.
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( (props, ref) => <input ref={ref} {...props} /> ); Input.displayName = "Input";
TypeScript With React Patterns
(continued)Compound Components
Build a compound component by attaching child components as static properties.
function Table({ children }: { children: React.ReactNode }) { return <table>{children}</table>; } Table.Row = function Row({ children }: { children: React.ReactNode }) { return <tr>{children}</tr>; }; Table.Cell = function Cell({ children }: { children: React.ReactNode }) { return <td>{children}</td>; };Use dot-notation at the call site to keep related components grouped.
<Table> <Table.Row> <Table.Cell>Name</Table.Cell> <Table.Cell>Age</Table.Cell> </Table.Row> </Table>Share state between compound parts using context.
const AccordionCtx = React.createContext<{ open: string; setOpen: (id: string) => void } | null>(null); function Accordion({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(""); return <AccordionCtx.Provider value={{ open, setOpen }}>{children}</AccordionCtx.Provider>; }Type the static child properties explicitly on the parent component's type.
interface TableComponent { (props: { children: React.ReactNode }): JSX.Element; Row: (props: { children: React.ReactNode }) => JSX.Element; Cell: (props: { children: React.ReactNode }) => JSX.Element; } const Table: TableComponent = ({ children }) => <table>{children}</table>;Access shared context inside a child component to read parent state.
Accordion.Item = function Item({ id, title, children }: { id: string; title: string; children: React.ReactNode }) { const ctx = useContext(AccordionCtx)!; return ( <div> <button onClick={() => ctx.setOpen(id)}>{title}</button> {ctx.open === id && children} </div> ); };
TypeScript With React Patterns
(continued)Type Guards in Components
Write a type guard function to narrow a union type inside a component.
interface User { kind: "user"; name: string; } interface Admin { kind: "admin"; name: string; permissions: string[]; } function isAdmin(person: User | Admin): person is Admin { return person.kind === "admin"; }Use the in operator as an inline type guard to check for a key's presence.
function ProfileCard({ person }: { person: User | Admin }) { return ( <div> <p>{person.name}</p> {"permissions" in person && <p>Permissions: {person.permissions.join(", ")}</p>} </div> ); }Narrow a discriminated union inside render using the kind field.
function Notification(props: { type: "info"; text: string } | { type: "alert"; text: string; level: number }) { if (props.type === "alert") { return <div className={`alert-${props.level}`}>{props.text}</div>; } return <div>{props.text}</div>; }Assert a type inside an event handler when you know the target element.
function Form() { const handleChange = (e: React.ChangeEvent<HTMLElement>) => { if (e.target instanceof HTMLInputElement) { console.log(e.target.value); // now typed as string } }; return <input onChange={handleChange} />; }Use a type guard to safely render only the correct variant of a union prop.
type Content = { kind: "text"; body: string } | { kind: "image"; src: string; alt: string }; function isImageContent(c: Content): c is Extract<Content, { kind: "image" }> { return c.kind === "image"; } function Render({ content }: { content: Content }) { return isImageContent(content) ? <img src={content.src} alt={content.alt} /> : <p>{content.body}</p>; }
TypeScript With React Patterns
(FAQ)FAQ
Prefer explicit return types like : JSX.Element or : React.ReactElement over React.FC<Props> — React.FC implicitly includes children in older versions and adds unnecessary complexity. Typing the function directly gives you cleaner inference and more control.
Use React.ChangeEvent<HTMLInputElement> as the event type: onChange: (e: React.ChangeEvent<HTMLInputElement>) => void. React exports event types for all DOM elements, so swap HTMLInputElement for the appropriate element type like HTMLSelectElement or HTMLTextAreaElement.
Declare a type parameter on the function: function List<T>({ items, render }: { items: T[]; render: (item: T) => React.ReactNode }). This lets callers pass any array type while still getting full type safety on the items.
Both work equally well for component props, but interface supports declaration merging, which is useful when library consumers need to extend your prop types. Use type when you need union types or mapped types that interface can't express.
Add as const to the returned array: return [value, setValue] as const — without it, TypeScript widens the type to (string | Dispatch<...>)[] instead of the precise tuple type. Alternatively, explicitly annotate the return type as [string, Dispatch<SetStateAction<string>>].
TypeScript Advanced Types
Learn conditional types, mapped types, template literals, and advanced pattern matching.
TL;DR
- 01Use conditional types for type-level if-else logic.
- 02Use mapped types to transform keys and values in existing types.
- 03Use template literal types for string manipulation at the type level.
Tips
- 01Use conditional types with infer to extract and reuse parts of complex types in your type transformations.
Warnings
- 01Advanced types are powerful but can become hard to read — document complex type transformations and keep them focused on single concerns.
TypeScript Advanced Types
(continued)Conditional Types
Write type-level if-else logic using the T extends X ? Y : Z ternary syntax.
type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<number>; // falseUse infer to capture and reuse a type nested inside a generic structure.
type Flatten<T> = T extends Array<infer U> ? U : T; type Str = Flatten<string[]>; // string type Num = Flatten<number>; // numberExtract function return types without importing the function itself.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type NumReturn = ReturnType<() => number>; // number type StrReturn = ReturnType<() => string>; // stringDistribute conditional types over union members automatically.
type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // string[] | number[]Prevent distribution by wrapping T in a tuple when you need one result.
type IsUnion<T> = [T] extends [infer U] ? (U extends T ? false : true) : false; type A = IsUnion<string | number>; // true type B = IsUnion<string>; // false
TypeScript Advanced Types
(continued)Mapped Types
Transform every key in an object type by iterating over keyof.
type MyReadonly<T> = { readonly [K in keyof T]: T[K]; }; type User = { name: string; age: number }; type ReadonlyUser = MyReadonly<User>; // { readonly name: string; readonly age: number }Make all properties optional by adding ? in a mapped type.
type Optional<T> = { [K in keyof T]?: T[K]; }; type PartialUser = Optional<User>; // { name?: string; age?: number }Rename keys with the as clause using template literal types.
type Getters<T> = { [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]; }; type UserGetters = Getters<User>; // { getName: () => string; getAge: () => number }Filter keys by combining mapped types with conditional types.
type StringKeys<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; type User = { id: number; name: string; email: string }; type StringOnly = StringKeys<User>; // { name: string; email: string }Build a Setters utility by inverting Getters with the mapped as pattern.
type Setters<T> = { [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void; }; type UserSetters = Setters<User>; // { setName: (value: string) => void; setAge: (value: number) => void }
TypeScript Advanced Types
(continued)Template Literal Types
Build string types at the type level using backtick template syntax.
type Event<T extends string> = `on${Capitalize<T>}`; type ClickEvent = Event<"click">; // "onClick" type ChangeEvent = Event<"change">; // "onChange"Combine a union with a template to generate all string combinations.
type Path = "user" | "post" | "comment"; type GetRoute = `GET /${Path}`; // "GET /user" | "GET /post" | "GET /comment"Extract the prefix of a string type with infer inside a template.
type GetPrefix<T extends string> = T extends `${infer Prefix}_${string}` ? Prefix : never; type P = GetPrefix<"user_id">; // "user" type Q = GetPrefix<"post_slug">; // "post"Split a string type into a tuple using recursive conditional types.
type Split<T extends string, D extends string> = T extends `${infer F}${D}${infer R}` ? [F, ...Split<R, D>] : [T]; type Parts = Split<"a-b-c", "-">; // ["a", "b", "c"]Build CSS-property-like types from a set of base names.
type Side = "top" | "right" | "bottom" | "left"; type Margin = `margin-${Side}`; // "margin-top" | "margin-right" | "margin-bottom" | "margin-left"
TypeScript Advanced Types
(continued)Advanced Utility Patterns
Build DeepPartial to make every nested property optional recursively.
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;Build DeepReadonly to freeze every level of a nested object type.
type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;Check type assignability at the type level with a boolean predicate.
type IsAssignableTo<T, U> = T extends U ? true : false; type A = IsAssignableTo<"hello", string>; // true type B = IsAssignableTo<number, string>; // falseBuild RequireKeys to make selected optional fields required.
type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>; type User = { id?: number; name?: string; email?: string }; type UserWithId = RequireKeys<User, "id">; // { id: number; name?: string; email?: string }Build a Validator type that maps each key to a validation function.
type Validator<T> = { [K in keyof T]: (value: T[K]) => boolean; }; // Usage: const userValidator: Validator<User> = { name: (v) => v.length > 0 };
TypeScript Advanced Types
(continued)Practical Applications
Build type-safe API response types using conditional types on status.
type ApiResponse<T extends "success" | "error"> = T extends "success" ? { status: 200; data: unknown } : { status: 400; error: string }; type Ok = ApiResponse<"success">; // { status: 200; data: unknown }Create a database query builder type using mapped types with as.
type SelectBuilder<T> = { [K in keyof T as `select${Capitalize<K & string>}`]: () => T[K]; }; // SelectBuilder<User> gives: { selectName: () => string; selectId: () => number }Extract promise resolution types for async function return values.
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type Data = Awaited<Promise<Promise<string>>>; // stringBuild a type-safe event map using template literals and mapped types.
type Events = { click: MouseEvent; keydown: KeyboardEvent }; type OnEvents = { [K in keyof Events as `on${Capitalize<K & string>}`]: (e: Events[K]) => void; }; // { onClick: (e: MouseEvent) => void; onKeydown: (e: KeyboardEvent) => void }Use conditional types to unwrap nested result types safely.
type UnwrapResult<T> = T extends { ok: true; value: infer V } ? V : T extends { ok: false; error: infer E } ? never : never;
TypeScript Advanced Types
(FAQ)FAQ
Use infer within an extends clause to capture a type variable: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never. This lets you pull out nested types like promise resolutions or function parameters without manually specifying them.
Mapped types iterate over keys to transform an existing type's structure (e.g., making all properties optional), while conditional types act like type-level ternaries to choose between types based on a condition. You often combine them — a mapped type whose values use a conditional type — for powerful transformations.
Yes — define a type like type EventName = on${Capitalize
TypeScript limits recursive type instantiation depth to prevent infinite loops; this typically happens with unbounded recursive conditional types. Break the recursion with a depth counter tuple or split the type into smaller, composable helper types to stay within the limit.
Combine mapped types with conditional types: type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>. This preserves optional fields outside K while enforcing presence for the keys you specify.
TypeScript Decorators
Understand decorators, metadata, class transformations, and real-world patterns.
TL;DR
- 01Use decorators to annotate and modify classes and methods.
- 02TypeScript 5.0+ supports standard decorators natively — use
experimentalDecoratorsonly for legacy frameworks (NestJS/Angular with older setups) - 03Apply metadata to decorate classes for frameworks and libraries.
Tips
- 01Use decorators sparingly and for cross-cutting concerns like logging, validation, or caching — avoid using them for simple business logic.
Warnings
- 01Standard decorators (TypeScript 5.0+, TC39 Stage 3) are stable. Only use
experimentalDecorators: trueif you're working with NestJS, Angular, or other frameworks that haven't yet migrated to the standard.
TypeScript Decorators
(continued)Class Decorators
- Standard decorators work in TypeScript 5.0+ without any tsconfig flag. Use
experimentalDecoratorsonly for legacy frameworks (NestJS, Angular with older setups).# TypeScript 5.0+: no flag needed for standard decorators # Legacy only (NestJS / Angular): - Enable legacy decorators in tsconfig.json when required by your framework.
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } } - Create a class decorator that receives the class constructor.
function Sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype); } @Sealed class User { name = "Alice"; } - Modify or wrap classes to add functionality.
function Timestamped<T extends { new(...args: any[]): {} }>( constructor: T ) { return class extends constructor { createdAt = new Date(); }; } - Factory decorators can return new classes with enhanced features.
TypeScript Decorators
(continued)Method and Property Decorators
- Create method decorators to intercept function calls.
function LogCalls( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const original = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling ${propertyKey}`, args); return original.apply(this, args); }; } class User { @LogCalls setName(name: string) { this.name = name; } } - Property decorators receive the target and property name.
function Validate(target: any, propertyKey: string) { let value: any; Object.defineProperty(target, propertyKey, { get() { return value; }, set(newVal: any) { if (newVal.length < 3) throw new Error("Too short"); value = newVal; } }); }
TypeScript Decorators
(continued)Metadata and Reflection
- Store metadata on classes and methods using the reflect-metadata library.
import "reflect-metadata"; function Route(path: string) { return function(target: any, propertyKey: string) { Reflect.defineMetadata("route:path", path, target, propertyKey); }; } class Controller { @Route("/users") getUsers() { } } - Retrieve metadata to build frameworks and libraries.
const path = Reflect.getMetadata("route:path", Controller.prototype, "getUsers"); console.log(path); // "/users" - Use emitDecoratorMetadata to capture parameter and return types.
function ValidateTypes(target: any, propertyKey: string) { const types = Reflect.getMetadata("design:paramtypes", target, propertyKey); // Types are [String, Number, etc.] }
TypeScript Decorators
(continued)Decorator Composition
- Stack multiple decorators on the same class or method.
@Sealed @Timestamped class User { @LogCalls @Validate setName(name: string) { this.name = name; } } - Decorators execute from bottom to top on the target.
- Compose decorators to build complex functionality.
function Chain(...decorators: any[]) { return (target: any, propertyKey: string) => { decorators.forEach(d => d(target, propertyKey)); }; } - Use composition to keep decorators focused on single concerns.
TypeScript Decorators
(continued)Real-World Patterns
- Build validation decorators for class properties.
function MinLength(min: number) { return function(target: any, propertyKey: string) { Reflect.defineMetadata(`validate:${propertyKey}`, { min }, target); }; } class User { @MinLength(3) name: string; } - Create caching decorators for expensive methods.
function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const original = descriptor.value; const cache = new Map(); descriptor.value = function(...args: any[]) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = original.apply(this, args); cache.set(key, result); return result; }; } - Use decorators in frameworks like NestJS for dependency injection.
TypeScript Decorators
(FAQ)FAQ
Add "experimentalDecorators": true to your tsconfig.json compilerOptions. If you need runtime metadata (e.g., for dependency injection), also enable "emitDecoratorMetadata": true and install the reflect-metadata package.
A class decorator receives the constructor function and can replace or modify the entire class, while a method decorator receives the target prototype, the method name, and its property descriptor — making it better suited for wrapping or intercepting individual method calls.
When composing decorators, they are applied bottom-up (innermost first) but evaluated top-down, so @A @B on a class means B's factory runs first but A's decorator function wraps the result last — the outermost decorator has the final say on the output.
Yes — combine a parameter decorator with a method decorator: the parameter decorator marks which params need validation using Reflect.metadata, and the method decorator wraps the function to check those marked params before execution runs.
No — TypeScript's legacy decorators (experimentalDecorators) follow an older spec and behave differently from the Stage 3 TC39 proposal. TypeScript 5.0+ supports the new standard decorators without the flag, but the APIs are incompatible, so migrating existing decorator code requires careful refactoring.
TypeScript Function Overloading
A reference for declaring multiple type signatures on a single TypeScript function.
TL;DR
- 01Declare multiple overload signatures before the implementation function.
- 02Implement the function body using union types to handle all signatures.
- 03Callers see only the matching overload, not the implementation signature.
Tips
- 01Use overloading when a function behaves differently based on input types — it's better than union types for callers.
Warnings
- 01Overloads are compile-time only — the implementation still needs to handle all types at runtime.
TypeScript Function Overloading
(continued)Basic Function Overloading
- Declare multiple overload signatures before the implementation.
// Overload signatures function add(a: number, b: number): number; function add(a: string, b: string): string; // Implementation function add(a: any, b: any) { return a + b; } add(1, 2); // OK: returns number add('a', 'b'); // OK: returns string - The implementation signature is not callable by consumers.
function format(value: string): string; function format(value: number): string; function format(value: string | number): string { return String(value); } // format(true); // error — implementation signature is hidden - Use at least two overload signatures before the implementation.
// One overload is not useful — use two or more function parse(input: string): number; function parse(input: number): string; function parse(input: string | number): string | number { return typeof input === "string" ? Number(input) : String(input); } - Overloads must be compatible with the implementation signature.
function wrap(value: string): string[]; function wrap(value: number): number[]; function wrap(value: string | number): string[] | number[] { return [value as any]; } - Use overloads to express arity differences explicitly.
function createUser(name: string): User; function createUser(name: string, role: string): User; function createUser(name: string, role?: string): User { return { name, role: role ?? "user" }; }
TypeScript Function Overloading
(continued)Optional Parameters
- Use overloads to give different return types per argument count.
function greet(name: string): string; function greet(name: string, age: number): string; function greet(name: string, age?: number) { if (age) { return `Hello, ${name}! You are ${age}.`; } return `Hello, ${name}!`; } greet('Alice'); // OK greet('Bob', 30); // OK - Overloads prevent callers from passing invalid argument combinations.
function connect(host: string): Connection; function connect(host: string, port: number): Connection; function connect(host: string, port?: number): Connection { return new Connection(host, port ?? 80); } - Optional overloads are clearer than one signature with many optional params.
// Hard to reason about — too many optional flags // function render(a: string, b?: string, c?: string, d?: number): void // Better: separate overloads for each valid call pattern function render(template: string): void; function render(template: string, context: Record<string, string>): void; function render(template: string, context?: Record<string, string>): void { // implementation } - Name overload parameters consistently for readability.
function fetch(url: string): Promise<Response>; function fetch(url: string, options: RequestInit): Promise<Response>; function fetch(url: string, options?: RequestInit): Promise<Response> { return window.fetch(url, options); } - Add a default-return overload last to handle the fallback case.
function get(key: "count"): number; function get(key: "label"): string; function get(key: string): unknown; function get(key: string): unknown { return store[key]; }
TypeScript Function Overloading
(continued)Different Return Types
- Overloads let callers get the right return type per input type.
function process(input: string): string; function process(input: number): number; function process(input: string | number): string | number { if (typeof input === 'string') { return input.toUpperCase(); } return input * 2; } const str = process('hello'); // string const num = process(5); // number - Without overloads, callers receive a union return type.
// Without overloads — caller must narrow the return function convert(x: string | number): string | number { return typeof x === "string" ? x.toUpperCase() : x * 2; } const result = convert("hi"); // string | number — inconvenient - Map input literal types to different output types with overloads.
function read(format: "json"): object; function read(format: "text"): string; function read(format: "buffer"): ArrayBuffer; function read(format: string): object | string | ArrayBuffer { // implementation chooses based on format return {}; } - Use generics when the return type depends on the input type generically.
// Overload approach function identity(x: string): string; function identity(x: number): number; function identity(x: any): any { return x; } // Generic approach — simpler for symmetric cases function identityG<T>(x: T): T { return x; } - Combine overloads with optional flags for conditional return shapes.
function search(query: string, raw: true): string[]; function search(query: string, raw?: false): SearchResult[]; function search(query: string, raw?: boolean): string[] | SearchResult[] { const results = queryIndex(query); return raw ? results.map(r => r.raw) : results; }
TypeScript Function Overloading
(continued)Complex Overloads
- Use overloads for functions that accept multiple input shapes.
function merge(obj1: object, obj2: object): object; function merge(arr1: any[], arr2: any[]): any[]; function merge(item1: any, item2: any) { if (Array.isArray(item1) && Array.isArray(item2)) { return [...item1, ...item2]; } return { ...item1, ...item2 }; } - Add overloads for callback vs non-callback variants of a function.
function load(id: number): Promise<User>; function load(id: number, cb: (user: User) => void): void; function load(id: number, cb?: (user: User) => void): Promise<User> | void { const p = fetchUser(id); if (cb) { p.then(cb); } else { return p; } } - Overload class methods the same way as standalone functions.
class Formatter { format(value: string): string; format(value: number): string; format(value: string | number): string { return String(value).trim(); } } - Order overloads from most specific to least specific.
function normalize(input: string[]): string[]; function normalize(input: string): string; function normalize(input: string | string[]): string | string[] { return Array.isArray(input) ? input.map(s => s.trim()) : input.trim(); } - Overload interface call signatures for typed function objects.
interface Converter { (value: string): number; (value: number): string; } const convert: Converter = (value: any) => typeof value === "string" ? Number(value) : String(value);
TypeScript Function Overloading
(continued)Best Practices
- Keep overloads focused so each signature is clearly distinct.
// Good: overloads are distinct function getValue(key: string): string; function getValue(key: number): number; // Avoid: overlapping overloads confuse callers // function getValue(key: string | number): any { } - Avoid overloads when a generic achieves the same result more simply.
// Overloads: verbose for symmetric relationships function echo(x: string): string; function echo(x: number): number; // Generic: cleaner for identity-like functions function echo<T>(x: T): T { return x; } - Document each overload signature with JSDoc for IDE tooltips.
/** Converts input to uppercase */ function transform(input: string): string; /** Doubles a numeric input */ function transform(input: number): number; function transform(input: string | number): string | number { return typeof input === "string" ? input.toUpperCase() : input * 2; } - Test every declared overload in your test suite.
// Verify each overload signature independently const a: string = transform("hello"); // must be string const b: number = transform(5); // must be number - Don't create an overload if a union parameter already expresses the intent.
// Use union when both inputs produce the same output type function printValue(value: string | number): void { console.log(String(value)); }
TypeScript Function Overloading
(FAQ)FAQ
The overload signatures are what TypeScript checks against — if none of them match the argument types you're passing, the call fails at compile time even if your implementation would handle it. Add an overload signature that covers the case, or widen an existing one.
Use overloading when the return type changes based on input type, or when you want callers to get a precise return type without casting. Union parameters are simpler when the behavior and return type are the same regardless of input.
No — the implementation signature is invisible to callers. Only the overload signatures above it are part of the public API, so you must explicitly declare every callable variant before the implementation.
Declare multiple method signatures with the same name inside the class body, then write a single implementation that accepts union types. The syntax is identical to standalone function overloading — no 'function' keyword needed.
Not directly — the overload syntax only works with function declarations and class methods. For arrow functions, define an overloaded call signature on an interface or type alias and then assign a compatible function to a variable of that type.
TypeScript Performance Tips
Profile code, optimize types, and identify performance bottlenecks in typed applications.
TL;DR
- 01Profile build time using tsc --diagnostics and --generateTrace flags.
- 02Avoid deeply nested generic types and large union types.
- 03Use import type to eliminate runtime overhead in compiled output.
Tips
- 01Profile your TypeScript compilation regularly to catch performance regressions early — small type optimizations add up.
Warnings
- 01Don't sacrifice type safety for build speed — focus on structural improvements, not disabling type checking.
TypeScript Performance Tips
(continued)Profiling Compilation
Run tsc with diagnostics to measure time spent in each compilation phase.
tsc --diagnostics # Prints: Files, Lines, Nodes, Identifiers, Symbols, Types, Check timeGenerate a trace file and open it in Chrome for deep performance analysis.
tsc --generateTrace ./trace # Open chrome://tracing and load ./trace/trace.jsonEnable incremental compilation to cache results and skip unchanged files.
{ "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./build/.tsbuildinfo" } }Measure total build time with the time command to establish a baseline.
time tsc # real 0m4.321s — record this before making changesUse extendedDiagnostics for a detailed per-phase breakdown of build time.
tsc --extendedDiagnostics # Outputs: I/O Read, I/O Write, Parse, Bind, Check, Emit
TypeScript Performance Tips
(continued)Optimizing Type Definitions
Avoid deeply nested generics — extract named intermediate types to reduce compiler work.
// Bad: deeply nested forces repeated re-evaluation type Complex = Array<Record<string, Array<Promise<Result>>>>; // Good: named intermediate types are cached by the compiler type ResultPromise = Promise<Result>; type ResultMap = Record<string, ResultPromise[]>; type Complex = ResultMap[];Keep union types small by grouping related members into subtypes.
// Bad: large union is slow to distribute over type Status = "ok" | "done" | "error" | "failed"; // ... many more // Good: grouped subtypes are faster to check type SuccessStatus = "ok" | "done"; type ErrorStatus = "error" | "failed"; type Status = SuccessStatus | ErrorStatus;Use const assertions to narrow literal types without adding runtime code.
const roles = ["admin", "user", "guest"] as const; type Role = typeof roles[number]; // "admin" | "user" | "guest"Prefer interface over type alias for object shapes — interfaces are cached by TypeScript.
// interface: TypeScript caches the shape for faster re-use interface Config { host: string; port: number; } // type alias: re-evaluated each time it appears inline type Config = { host: string; port: number };Extract shared subtypes to avoid repeating complex inline shapes in generics.
// Better: shared subtype is computed and cached once type PageMeta = { total: number; page: number }; type Response<T> = { data: T; meta: PageMeta };
TypeScript Performance Tips
(continued)Type-Only Imports
Use import type to import types only — the compiler erases them at emit time.
import type { User, Product } from "./types"; const user: User = { id: 1, name: "Alice" };Use inline type imports when mixing value and type imports from one module.
import { readFile, type FileHandle } from "fs/promises"; // readFile is a runtime value; FileHandle is erased at emitEnable verbatimModuleSyntax in tsconfig to enforce type-only imports at compile time.
{ "compilerOptions": { "verbatimModuleSyntax": true } }Use type-only re-exports to avoid pulling in runtime module side effects.
// Safe: no runtime import of the whole module export type { User } from "./models/user"; // Risky: may import the entire module at runtime export { User } from "./models/user";Enforce no unused imports with ESLint to keep compiled output lean.
{ "rules": { "@typescript-eslint/no-unused-vars": "error" } }
TypeScript Performance Tips
(continued)Structural Sharing
Use declaration merging to extend an interface across multiple files cleanly.
interface User { id: number; } interface User { name: string; } // User now has both id and name — no duplication neededExtend base interfaces to share common fields across all entity types.
interface BaseEntity { id: number; createdAt: Date; } interface User extends BaseEntity { name: string; } interface Post extends BaseEntity { title: string; }Create a shared types file to avoid re-declaring the same shapes.
// types/shared.ts export interface Pagination { page: number; size: number; total: number; } // Import once, use in multiple files import type { Pagination } from "../types/shared";Compose types with intersection to combine existing shapes without duplication.
type Audited = { createdBy: string; updatedBy: string }; type Product = { id: number; name: string }; type AuditedProduct = Product & Audited;Use mapped types to derive Partial and Readonly variants from one source interface.
interface User { id: number; name: string; email: string; } type UserPartial = Partial<User>; // all fields optional type UserReadonly = Readonly<User>; // all fields readonly
TypeScript Performance Tips
(continued)Build Tool Integration
Use esbuild for extremely fast bundling — it transpiles TypeScript without type checking.
npm install -D esbuild esbuild src/index.ts --bundle --outfile=dist/index.jsSet skipLibCheck to skip type-checking of node_modules declaration files.
{ "compilerOptions": { "skipLibCheck": true } }Use ts-loader with transpileOnly to skip type checking during webpack builds.
{ loader: 'ts-loader', options: { transpileOnly: true, // skips type checking, much faster experimentalWatchApi: true } }Use SWC to transpile fast, then run tsc separately for type checking only.
# Transpile fast with SWC, check types in a separate step swc src -d dist tsc --noEmit # type checking only, no JS outputUse project references to split monorepos into independently cached packages.
{ "references": [ { "path": "./packages/core" }, { "path": "./packages/ui" } ], "compilerOptions": { "composite": true } }
TypeScript Performance Tips
(FAQ)FAQ
Run tsc --diagnostics or tsc --extendedDiagnostics to get a breakdown of time spent on each compilation phase. For deeper analysis, use --generateTrace to produce a trace file you can load in Chrome DevTools.
Type-only imports (import type { Foo } from './foo') are erased entirely at emit time, so they never appear in compiled JavaScript. This reduces bundle size and prevents accidental runtime dependencies on modules that only contain types.
Deeply nested generics force the compiler to recursively instantiate and check complex type structures, which can exponentially increase type-checking time. Flatten or break them into intermediate named type aliases to give the compiler checkpoints and reduce redundant work.
Use project references (composite: true with tsc --build) so each package is compiled incrementally and results are cached. This avoids re-checking unchanged packages on every build and enables parallel compilation across projects.
skipLibCheck: true skips type-checking of .d.ts files in node_modules, which can meaningfully cut build time in large projects. It's a safe tradeoff in most cases since library types are typically pre-validated — just ensure your own source files still get full checking.
TypeScript Utility Types
A practical reference for TypeScript's built-in utility types including Partial, Pick, Omit, Record, ReturnType, and how to compose custom utilities.
TL;DR
- 01Utility types are generic helpers built into TypeScript that transform existing types into new ones without duplicating code.
- 02The most-used utilities — Partial, Required, Readonly, Pick, Omit, and Record — cover the vast majority of everyday type-transformation needs.
- 03You can compose multiple utility types together or build your own using mapped types and conditional types for any pattern the built-ins do not handle.
Tips
- 01Hover over any utility type in VS Code and TypeScript will expand the resolved type in the tooltip — a fast way to verify the transformation behaves exactly as you expect without running the compiler.
- 02Use <code>Omit</code> rather than <code>Pick</code> when extending a third-party interface — you avoid breakage when upstream adds new fields you would want to keep automatically without touching your type definition.
- 03<code>Awaited<T></code> (TypeScript 4.5+) recursively unwraps nested <code>Promise</code> wrappers. Combine it with <code>ReturnType</code> as <code>Awaited<ReturnType<typeof myFn>></code> to get the resolved value type of any async function in a single expression.
Warnings
- 01<code>Readonly<T></code> does not freeze the object at runtime — it only prevents compile-time reassignment. Use <code>Object.freeze()</code> for runtime immutability, or write a recursive <code>DeepReadonly</code> utility if nested object mutation must also be caught by the compiler.
- 02Deeply recursive utility types can slow TypeScript's language server noticeably on large union or object types. If autocomplete lags, add a depth-counter type parameter to short-circuit recursion after 5–10 levels.
TypeScript Utility Types
(continued)What Utility Types Are
Utility types are generic type aliases shipped with TypeScript's standard library that accept one or more type arguments and return a transformed version of that type. They prevent you from hand-writing repetitive mapped types and keep your codebase consistent across teams.
All utility types are purely compile-time constructs — they emit zero JavaScript. They work by combining three lower-level features: mapped types ({ [K in keyof T]: ... }), conditional types (T extends U ? X : Y), and the infer keyword for extracting nested type information at the structural level.
| Utility Type | Input | What It Returns | Since TS |
|---|---|---|---|
Partial<T> | Object type | All properties made optional | 2.1 |
Required<T> | Object type | All optional props made required | 2.8 |
Readonly<T> | Object type | All properties read-only | 2.1 |
Pick<T, K> | Object type + key union | Subset of T with only keys K | 2.1 |
Omit<T, K> | Object type + key union | T minus keys K | 3.5 |
Record<K, V> | Key union + value type | Object with keys K and values V | 2.1 |
ReturnType<T> | Function type | Function's return type | 2.8 |
Awaited<T> | Promise or plain type | Recursively unwrapped value type | 4.5 |
TypeScript Utility Types
(continued)Partial, Required, and Readonly
Partial<T> makes every property in T optional by adding the ? modifier. It is most useful when writing update or PATCH functions where only a subset of fields may be present. It is shallow — nested object types are not recursively made optional.
Required<T> strips the ? modifier from every property, enforcing a fully-populated model. Use it after a factory function fills in defaults or after a validation step guarantees all fields are present.
Readonly<T> adds the readonly modifier to every property, causing the compiler to reject any mutations. Like the others, it is shallow by default.
| Utility | Modifier Applied | Common Use Case | Deep? |
|---|---|---|---|
Partial<T> | Adds ? to all props | PATCH request body, form draft state | No |
Required<T> | Removes ? from all props | Post-validation model, factory output | No |
Readonly<T> | Adds readonly to all props | Immutable config objects, Redux state | No |
- Use
Partial<Pick<T, K>>to make only a specific subset of keys optional — a common pattern for form field groups. - The
-?modifier in a custom mapped type removes optionality on a per-property basis, giving finer control thanRequired.
TypeScript Utility Types
(continued)Pick, Omit, and Extract
Pick<T, K> constructs a new type containing only the keys listed in the union K. It is the right tool when a function or component needs an explicit subset of a larger model, making the dependency surface clear and narrow.
Omit<T, K> does the opposite — it removes listed keys and returns everything else. Prefer Omit when the excluded set is small but the kept set is large, especially when extending third-party interfaces whose future fields you want to keep automatically.
Extract<T, U> and its counterpart Exclude<T, U> operate on union types, not object types. Extract keeps union members assignable to U; Exclude removes them. NonNullable<T> is shorthand for Exclude<T, null | undefined>.
| Utility | Operates On | Example Input | Result |
|---|---|---|---|
Pick<T, K> | Object type | Pick<User, 'id'|'name'> | { id: number; name: string } |
Omit<T, K> | Object type | Omit<User, 'password'> | User without the password field |
Extract<T, U> | Union type | Extract<'a'|'b'|'c', 'a'|'c'> | 'a' | 'c' |
Exclude<T, U> | Union type | Exclude<'a'|'b'|'c', 'a'> | 'b' | 'c' |
NonNullable<T> | Union type | NonNullable<string|null> | string |
TypeScript Utility Types
(continued)Record, ReturnType, and Parameters
Record<K, V> builds an object type whose keys are members of the union K and whose values are all of type V. It is ideal for lookup tables, enum-keyed maps, and exhaustive switch alternatives where every key must be explicitly handled.
ReturnType<T> extracts the return type of a function type. This is invaluable when you depend on a function's output shape but cannot import its return type directly — for example, with third-party library factory functions that do not export their types.
Parameters<T> extracts parameter types as a tuple. ConstructorParameters<T> does the same for class constructors. InstanceType<T> extracts the class instance type from a constructor. Pair these with spread syntax to forward arguments type-safely through wrappers.
| Utility | Input Example | Resolves To |
|---|---|---|
Record<'USD'|'EUR', number> | Key union + value type | { USD: number; EUR: number } |
ReturnType<typeof fetch> | Function type | Promise<Response> |
Awaited<ReturnType<typeof fetch>> | Async function type | Response |
Parameters<typeof parseInt> | Function type | [string, (number | undefined)?] |
InstanceType<typeof Date> | Constructor type | Date |
TypeScript Utility Types
(continued)Building Custom Utility Types
When the built-ins do not cover a pattern, build your own using mapped types, conditional types, and the infer keyword. Custom utilities follow the same generic signature as the built-ins and compose identically — they can be used as arguments to other utility types.
Common custom utilities include DeepPartial<T> (recursively optional), Mutable<T> (removes all readonly modifiers, useful in tests), and UnwrapPromise<T> (the pre-4.5 equivalent of Awaited). Keep these in a shared src/types/utils.ts file.
| Custom Utility | Definition Pattern | Use Case |
|---|---|---|
DeepPartial<T> | { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] } | Deeply nested patch payloads |
Mutable<T> | { -readonly [K in keyof T]: T[K] } | Strip readonly in test setup |
PickByValue<T, V> | Mapped + conditional type filtering by value type | Keep only string-valued props |
UnwrapPromise<T> | T extends Promise<infer R> ? R : T | Pre-TS 4.5 Awaited equivalent |
Nullable<T> | { [K in keyof T]: T[K] | null } | Database row types that allow NULL |
- Use the
-readonlyand-?modifier prefixes in mapped types to remove existing modifiers rather than add them. - Add type-level unit tests using the
tsdorexpect-typepackage to catch regressions when TypeScript is upgraded.
TypeScript Utility Types
(FAQ)FAQ
Pick creates a new type by selecting only the specified keys from a type, while Omit creates a new type by excluding the specified keys. Use Pick when you want a small subset of a large type, and Omit when you want most fields minus a few exceptions.
Yes, utility types are composable — for example, Partial<Pick<User, 'name' | 'email'>> creates a type with only those two fields made optional. You can chain as many transformations as needed to express complex type shapes concisely.
Use Record<K, V> when the set of keys is a known union type, such as Record<'admin' | 'user', Permission>, because it enforces that every key in the union is present. A plain index signature ({ [key: string]: V }) is more appropriate when keys are arbitrary and not enumerable at compile time.
ReturnType<T> extracts the return type of a function type, and Parameters<T> extracts its argument types as a tuple. Both are useful for deriving types from existing functions without duplicating type declarations, especially when working with third-party functions whose source types are not exported.
Custom utility types are written as generic types using mapped types (e.g., { [K in keyof T]: ... }) and conditional types (e.g., T extends U ? X : Y). For example, type Mutable<T> = { -readonly [K in keyof T]: T[K] } removes the readonly modifier from every property of T.