Complete TypeScript Tutorial
Master TypeScript with our comprehensive tutorial.
Getting Started with TypeScript: Your First Steps
Learn to set up TypeScript and create your first type-safe application
Key Concept: TypeScript is a superset of JavaScript that adds static typing. You write .ts files, compile them to .js, and run the JavaScript in any environment. TypeScript catches errors at compile time before your code runs.
What You'll Need
- Node.js: Download from nodejs.org (includes npm)
- Text Editor: VS Code (recommended - built-in TypeScript support)
- Terminal/Command Line: For running TypeScript compiler
- Basic JavaScript Knowledge: Understanding of JS fundamentals
Installing TypeScript
TypeScript is installed via npm (Node Package Manager). You can install it globally or per project.
# Install TypeScript globally
npm install -g typescript
# Verify installation
tsc --version
# Output: Version 5.3.3 (or latest)
# Create project directory
mkdir my-typescript-project
cd my-typescript-project
# Initialize npm project
npm init -y
# Install TypeScript as dev dependency
npm install --save-dev typescript
# Create TypeScript config
npx tsc --init
Your First TypeScript File
Let's create a simple TypeScript file and compile it to JavaScript.
// Define a function with typed parameters
function greet(name: string): string {
return `Hello, ${name}!`;
}
// TypeScript will catch type errors
const message = greet("TypeScript");
console.log(message);
// This would cause a compile error:
// greet(42); // Error: Argument of type 'number' not assignable to 'string'
Compiling TypeScript
Use the TypeScript compiler (tsc) to convert .ts files to .js files.
# Compile single file
tsc hello.ts
# Creates: hello.js
# Compile and watch for changes
tsc hello.ts --watch
# Compile with target ES version
tsc hello.ts --target ES2020
# Compile entire project using tsconfig.json
tsc
Understanding the Output
// Type annotations are removed
function greet(name) {
return `Hello, ${name}!`;
}
const message = greet("TypeScript");
console.log(message);
Setting Up VS Code for TypeScript
| Feature | Description | Shortcut |
|---|---|---|
| IntelliSense | Auto-completion and type info | Ctrl+Space |
| Go to Definition | Navigate to type definitions | F12 |
| Find References | Find all usages | Shift+F12 |
| Rename Symbol | Refactor names safely | F2 |
| Quick Fix | Auto-fix type errors | Ctrl+. |
Creating a TypeScript Configuration
The tsconfig.json file controls compiler behavior and project settings.
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Practice Tasks
- Task 1: Install TypeScript globally using npm.
- Task 2: Create hello.ts and add a typed greet function.
- Task 3: Compile hello.ts using tsc command.
- Task 4: Run the compiled JavaScript with node hello.js.
- Task 5: Initialize a project with tsconfig.json.
- Task 6: Create a calculator function with typed parameters.
- Task 7: Set up TypeScript watch mode and test live compilation.
Common Setup Issues
Troubleshooting
- tsc not found: Make sure Node.js and npm are installed, then reinstall TypeScript globally
- Permission errors: On Mac/Linux, use sudo:
sudo npm install -g typescript - Old TypeScript version: Update with:
npm install -g typescript@latest - VS Code not recognizing TypeScript: Reload window (Ctrl+Shift+P → "Reload Window")
Key Takeaways
- TypeScript = JavaScript + Types: All valid JS is valid TS
- Compile to JavaScript: TypeScript doesn't run directly
- Catch errors early: Type checking happens at compile time
- Use tsconfig.json: Configure compiler for your project
- VS Code recommended: Best TypeScript development experience
- Start simple: Add types gradually to existing projects
What's Next?
Next Topic: Now that you have TypeScript set up, let's explore what TypeScript is, why it was created, and how it enhances JavaScript development in the Introduction section.
TypeScript Introduction: Understanding Microsoft's JavaScript Enhancement
Discover why TypeScript has become the preferred choice for building scalable JavaScript applications
What is TypeScript? A Complete Overview
TypeScript is a strongly typed programming language that builds on JavaScript, developed and maintained by Microsoft. It's a superset of JavaScript, meaning any valid JavaScript code is also valid TypeScript. TypeScript adds optional static type checking, interfaces, enums, and advanced features that compile down to clean, readable JavaScript.
TypeScript was created to address the challenges of building and maintaining large-scale JavaScript applications. As codebases grow, JavaScript's dynamic typing can lead to runtime errors that are hard to catch during development. TypeScript solves this by introducing a type system that catches errors at compile time, providing developers with confidence and productivity gains.
Why TypeScript? Key Benefits
TypeScript has rapidly gained adoption among developers and organizations worldwide. Here's why TypeScript matters:
Type Safety
Catch errors at compile time before they reach production. TypeScript's type system prevents common bugs like null references, undefined properties, and type mismatches.
Enhanced Productivity
IntelliSense, auto-completion, and refactoring tools work better with type information, making development faster and reducing cognitive load.
Self-Documenting Code
Type annotations serve as inline documentation, making code more readable and maintainable. New developers can understand code structure instantly.
Modern JavaScript Features
Use the latest ECMAScript features today. TypeScript compiles to any JavaScript version, ensuring compatibility with older browsers.
TypeScript vs JavaScript: The Difference
| Aspect | JavaScript | TypeScript |
|---|---|---|
| Type System | Dynamic, runtime checking | Static, compile-time checking |
| Error Detection | Runtime errors | Compile-time errors |
| Tooling Support | Limited IntelliSense | Rich IntelliSense & refactoring |
| Learning Curve | Easier for beginners | Requires type system knowledge |
| Browser Support | Runs directly in browsers | Must be compiled to JavaScript |
| File Extension | .js | .ts |
TypeScript in Action: Before and After
// JavaScript - error only discovered when code runs
function calculateTotal(price, quantity) {
return price * quantity;
}
// This looks fine but will cause issues
calculateTotal("50", "3"); // Returns "5050" instead of 150
calculateTotal(null, 5); // Returns 0 (unexpected)
calculateTotal(); // Returns NaN (undefined * undefined)
// TypeScript catches errors before runtime
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
// TypeScript prevents these mistakes
calculateTotal("50", "3"); // ❌ Error: string not assignable to number
calculateTotal(null, 5); // ❌ Error: null not assignable to number
calculateTotal(); // ❌ Error: expected 2 arguments
// Only correct usage compiles
calculateTotal(50, 3); // ✅ Returns 150
Key TypeScript Features
- Static Typing: Add type annotations to variables, parameters, and return values
- Interfaces: Define contracts for object shapes and class implementations
- Classes & OOP: Full-featured object-oriented programming with access modifiers
- Generics: Write reusable, type-safe code that works with any data type
- Enums: Define named constants for better code readability
- Type Inference: Smart type detection without explicit annotations
- Union & Intersection Types: Combine types in powerful ways
- Advanced Types: Mapped types, conditional types, utility types
- Decorators: Meta-programming for classes and methods
- Modules & Namespaces: Organize code into logical units
Who Uses TypeScript?
TypeScript has been adopted by major tech companies and popular open-source projects:
🏢
Tech Giants
Microsoft, Google, Airbnb, Slack, Asana, Bloomberg
⚛️
Frameworks
Angular (built with TS), React, Vue 3, NestJS, Deno
📦
Libraries
RxJS, TypeORM, Jest, Prettier, ESLint
Real-World Example: User Management
// Define user structure with interface
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
// Function with strong typing
function createUser(name: string, email: string): User {
return {
id: Date.now(),
name,
email,
role: 'user',
createdAt: new Date()
};
}
// Type-safe array operations
const users: User[] = [];
users.push(createUser("Alice", "alice@example.com"));
// Autocomplete works perfectly
users[0].name; // ✅ IDE suggests: id, name, email, role, createdAt
users[0].age; // ❌ Error: Property 'age' does not exist
When to Use TypeScript
- Large-scale applications
- Team projects with multiple developers
- Long-term maintained codebases
- Complex business logic
- Libraries and frameworks
- When refactoring frequently
- Small prototypes or proofs of concept
- Simple scripts or one-off tools
- Projects with tight deadlines
- When team lacks TS experience
- Very dynamic codebases
- Quick experiments
Key Takeaways
- Superset of JavaScript: All JavaScript is valid TypeScript
- Optional static typing: Add types where they provide value
- Compiles to JavaScript: Works anywhere JavaScript runs
- Catches errors early: Type checking at development time
- Better tooling: Enhanced IDE support and refactoring
- Industry standard: Widely adopted for enterprise applications
- Future-proof: Use modern features while targeting older JS versions
What's Next?
Next Topic: Learn about TypeScript's evolution and development history. Understand how it went from a Microsoft internal project to one of the most popular programming languages.
TypeScript History: From Microsoft Project to Industry Standard
Explore the journey of TypeScript from its inception to becoming one of the world's most popular programming languages
The Birth of TypeScript (2012)
TypeScript was created at Microsoft in 2012 by Anders Hejlsberg, the legendary developer behind Turbo Pascal, Delphi, and C#. The project emerged from Microsoft's need to build large-scale JavaScript applications with the reliability and maintainability of statically-typed languages.
As JavaScript applications grew in complexity, Microsoft teams struggled with common issues: runtime type errors, poor refactoring support, and difficulty maintaining large codebases. TypeScript was born to address these challenges while maintaining full JavaScript compatibility.
Major Milestones
| Year | Version | Key Features |
|---|---|---|
| 2012 | 0.8 | Initial public release at Microsoft's Build conference |
| 2014 | 1.0 | First stable release, production-ready |
| 2015 | 1.5 | ES6 support, decorators, module resolution |
| 2016 | 2.0 | Non-nullable types, control flow analysis |
| 2017 | 2.4 | String enums, weak type detection |
| 2018 | 3.0 | Project references, tuple types |
| 2020 | 4.0 | Variadic tuple types, labeled tuples |
| 2023 | 5.0 | Decorators (stage 3), const type parameters |
| 2024 | 5.3+ | Import attributes, type-only imports refinements |
Anders Hejlsberg: The Architect
Anders Hejlsberg brought decades of programming language design experience to TypeScript. His previous work on C# heavily influenced TypeScript's design philosophy: maintaining simplicity while providing powerful features for large-scale development.
- Turbo Pascal (1980s): Fast, efficient compiler
- Delphi (1990s): Visual development environment
- C# (2000): Microsoft's flagship language
- TypeScript (2012): JavaScript with types
Growth and Adoption Timeline
Early Days (2012-2015)
- Initially met with skepticism
- Angular 2 adoption (2015) boosted popularity
- Google's internal adoption
- Growing community support
Mainstream Adoption (2016-2020)
- Major companies switching to TypeScript
- React officially supports TypeScript
- Vue 3 written in TypeScript
- Top 10 most popular languages
Industry Standard (2020-Present)
- 80%+ of npm packages provide TS types
- Default for new enterprise projects
- Stack Overflow's most loved language
- Over 6 million npm downloads per week
Global Impact
- Used by millions of developers
- GitHub's 4th most used language
- Essential for modern web development
- Influences other languages (Flow, Dart)
Key Technical Evolution
// Early TypeScript was simpler
function greet(name: string): string {
return "Hello, " + name;
}
interface Person {
name: string;
age: number;
}
// Modern TypeScript is incredibly powerful
type AsyncResult = Promise<{ data: T; error: null } | { data: null; error: Error }>;
function createFetcher>() {
return async (url: string): AsyncResult => {
try {
const response = await fetch(url);
const data = await response.json() as T;
return { data, error: null };
} catch (error) {
return { data: null, error: error as Error };
}
};
}
// Decorators (Stage 3 standard)
function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return original.apply(this, args);
};
}
Why TypeScript Succeeded
- JavaScript Compatibility: Any valid JS is valid TS - easy migration
- Gradual Adoption: Add types incrementally, no rewrite needed
- Excellent Tooling: First-class VS Code support from day one
- Open Source: Community-driven development on GitHub
- Framework Support: Angular, React, Vue all embrace TypeScript
- Regular Updates: New features every few months
- Backward Compatible: Upgrading rarely breaks existing code
- Type Definitions: DefinitelyTyped provides types for JavaScript libraries
Impact on the JavaScript Ecosystem
TypeScript fundamentally changed JavaScript development:
🛠️
Better Tooling
Inspired better IDE support for all JavaScript, not just TypeScript
📝
JSDoc Types
JavaScript can now use JSDoc comments for type checking without TS
📦
Type Definitions
80,000+ packages on DefinitelyTyped provide TypeScript types
Statistics and Adoption
By the Numbers (2024)
- 38% of npm packages are written in TypeScript
- 73% of developers want to continue using TypeScript (Stack Overflow Survey)
- Top 10 GitHub language by repositories and contributions
- 6+ million weekly downloads from npm
- 95%+ of Fortune 500 tech companies use TypeScript
- 4th most wanted language by developers learning new technologies
Key Takeaways
- Created by Anders Hejlsberg at Microsoft in 2012
- Evolved from experimental to industry standard in 12 years
- Regular updates maintain relevance and add features
- Open-source development model drives innovation
- JavaScript compatibility ensured widespread adoption
- Framework support accelerated mainstream acceptance
- Now essential skill for modern web developers
What's Next?
Next Topic: Now that you understand TypeScript's history and significance, let's dive into Basic Types and learn how to add type safety to your code.
TypeScript Basic Types: Foundation of Type Safety
Master the fundamental types that form the building blocks of TypeScript's type system
Understanding TypeScript Types
Types in TypeScript define what kind of values a variable can hold. TypeScript's type system includes primitive types (string, number, boolean), special types (any, unknown, void, never), and complex types (arrays, tuples, objects). Type annotations use colon syntax: variable: type.
Primitive Types
TypeScript provides types for all JavaScript primitives plus a few additions.
// String type for text values
let username: string = "Alice";
let email: string = 'alice@example.com';
let message: string = `Hello, ${username}!`; // Template literals
// Type inference - TypeScript infers string type
let city = "New York"; // type: string
city = "London"; // ✅ OK
city = 12345; // ❌ Error: Type 'number' is not assignable to type 'string'
// Number type for all numeric values
let age: number = 25;
let price: number = 99.99;
let hexValue: number = 0xf00d; // Hexadecimal
let binaryValue: number = 0b1010; // Binary
let octalValue: number = 0o744; // Octal
// Special numeric values
let infinity: number = Infinity;
let notANumber: number = NaN;
age = 26; // ✅ OK
age = "26"; // ❌ Error: Type 'string' is not assignable to type 'number'
// Boolean type for true/false values
let isActive: boolean = true;
let hasPermission: boolean = false;
// From expressions
let isAdult: boolean = age >= 18;
let isEmpty: boolean = items.length === 0;
isActive = false; // ✅ OK
isActive = "false"; // ❌ Error: Type 'string' is not assignable to type 'boolean'
isActive = 0; // ❌ Error: Type 'number' is not assignable to type 'boolean'
Arrays and Tuples
// Syntax 1: Type[]
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
// Syntax 2: Array (generic syntax)
let scores: Array = [95, 87, 92];
let tags: Array = ["typescript", "javascript"];
// Mixed types require union types
let mixed: (string | number)[] = ["age", 25, "name", "Alice"];
// Arrays of objects
let users: { name: string; age: number }[] = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 }
];
// Tuple: array with fixed length and specific types for each position
let person: [string, number] = ["Alice", 25];
let coordinate: [number, number] = [10.5, 20.3];
// Labeled tuples (TypeScript 4.0+)
let employee: [name: string, age: number, salary: number] = ["Bob", 30, 75000];
// Accessing tuple elements
console.log(person[0]); // "Alice" - type: string
console.log(person[1]); // 25 - type: number
// Type safety
person[0] = "Bob"; // ✅ OK
person[0] = 123; // ❌ Error: Type 'number' is not assignable to type 'string'
person[2] = "extra"; // ❌ Error: Tuple of length 2 has no element at index 2
// Optional tuple elements
let point: [number, number, number?] = [10, 20]; // z-coordinate is optional
Special Types
| Type | Description | When to Use |
|---|---|---|
any |
Disables type checking | Avoid when possible; use during migration or with dynamic data |
unknown |
Type-safe version of any | When you don't know the type but want safety |
void |
Absence of a return value | Functions that don't return anything |
never |
Value that never occurs | Functions that throw errors or infinite loops |
null |
Intentional absence of value | Explicitly represent "no value" |
undefined |
Variable not assigned | Optional parameters or properties |
// any: No type safety - avoid when possible
let anything: any = "hello";
anything = 42;
anything = true;
anything.nonExistent(); // ✅ No error, but will crash at runtime!
// unknown: Type-safe alternative
let something: unknown = "hello";
something = 42;
something = true;
// Must check type before using
if (typeof something === "string") {
console.log(something.toUpperCase()); // ✅ OK - type narrowed to string
}
something.toUpperCase(); // ❌ Error: Object is of type 'unknown'
// void: Function returns nothing
function logMessage(message: string): void {
console.log(message);
// No return statement
}
// Can explicitly return undefined
function doNothing(): void {
return undefined; // ✅ OK
}
// never: Function never returns
function throwError(message: string): never {
throw new Error(message);
// Never reaches end
}
function infiniteLoop(): never {
while (true) {
// Runs forever
}
}
// Exhaustiveness checking with never
type Shape = Circle | Square;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
const _exhaustive: never = shape; // Ensures all cases handled
return _exhaustive;
}
}
null and undefined
// With strictNullChecks enabled (recommended)
let username: string = "Alice";
username = null; // ❌ Error: Type 'null' is not assignable to type 'string'
username = undefined; // ❌ Error: Type 'undefined' is not assignable to type 'string'
// Explicitly allow null/undefined
let nullableString: string | null = "hello";
nullableString = null; // ✅ OK
let optionalString: string | undefined = "hello";
optionalString = undefined; // ✅ OK
// Both null and undefined
let flexible: string | null | undefined = "hello";
flexible = null; // ✅ OK
flexible = undefined; // ✅ OK
// Optional properties (shorthand for | undefined)
interface User {
name: string;
email?: string; // Same as: email: string | undefined
}
const user: User = { name: "Alice" }; // ✅ OK - email is optional
Type Inference
// TypeScript infers types without explicit annotations
let inferredString = "Hello"; // type: string
let inferredNumber = 42; // type: number
let inferredBoolean = true; // type: boolean
let inferredArray = [1, 2, 3]; // type: number[]
// Function return type inferred
function add(a: number, b: number) {
return a + b; // return type inferred as number
}
// Object type inferred
const person = {
name: "Alice",
age: 25
}; // type: { name: string; age: number }
// Best practice: Let TypeScript infer when obvious
let message = "Hello"; // ✅ Good - inference is clear
let message: string = "Hello"; // ⚠️ Redundant - type is obvious
Type Annotations Best Practices
- ✅ Function parameters: Always annotate - inference doesn't work
- ✅ Function return types: Be explicit for documentation and safety
- ✅ Object properties: Annotate when structure is complex
- ✅ Variables initialized later: TypeScript can't infer without initial value
- ❌ Simple variable assignments: Let TypeScript infer obvious types
- ❌ Return types of simple functions: Inference usually works perfectly
Practice Tasks
- Task 1: Create variables with string, number, and boolean types.
- Task 2: Create an array of numbers and an array of strings.
- Task 3: Define a tuple representing a person: [name, age, city].
- Task 4: Create a function that returns void and one that returns never.
- Task 5: Practice with nullable types: string | null.
- Task 6: Create an object and let TypeScript infer its type.
- Task 7: Use unknown type safely with type checking.
Common Mistakes to Avoid
⚠️ Type Pitfalls
- Overusing any: Defeats purpose of TypeScript
- Forgetting strictNullChecks: Enable it for better safety
- Over-annotating: Trust inference for simple cases
- Confusing tuples and arrays: Tuples have fixed length
- Using loose equality (==): Prefer strict (===) for type safety
Key Takeaways
- Primitive types: string, number, boolean
- Arrays: type[] or Array<type>
- Tuples: Fixed-length arrays with specific types per position
- any vs unknown: Prefer unknown for type safety
- void: Functions that don't return
- never: Functions that never return
- Type inference: Let TypeScript infer obvious types
- strictNullChecks: Enable for better null safety
What's Next?
Next Topic: Learn about Interfaces - TypeScript's way to define object shapes and contracts for your code.
TypeScript Interfaces: Defining Object Shapes
Master the art of creating type-safe contracts for objects, classes, and functions using TypeScript interfaces
What are Interfaces?
Interfaces in TypeScript are powerful contracts that define the structure of objects. They specify what properties and methods an object should have, along with their types. Unlike classes, interfaces exist only at compile-time and are used purely for type checking—they don't generate any JavaScript code.
Interfaces are one of TypeScript's core features for enforcing type safety and creating self-documenting code. They help catch errors early in development by ensuring objects conform to expected shapes, making your code more maintainable and less prone to runtime errors.
Why Use Interfaces?
Type Safety
Catch type-related bugs at compile time before they reach production, reducing runtime errors significantly.
Self-Documentation
Interfaces serve as clear contracts that document expected object structures, making code easier to understand.
IDE Support
Get excellent autocomplete, IntelliSense, and refactoring support when working with interfaces.
Reusability
Define once and reuse across multiple functions, classes, and modules for consistent type checking.
Basic Interface Syntax
Let's start with a simple interface definition:
Example: Basic Interface
// Define a simple interface
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Use the interface
const user: User = {
id: 1,
name: "Alice Johnson",
email: "alice@example.com",
age: 28
};
// This will cause an error - missing 'age' property
const invalidUser: User = {
id: 2,
name: "Bob Smith",
email: "bob@example.com"
// Error: Property 'age' is missing
};
Optional Properties
Use the ? symbol to make properties optional. This is useful when some properties may or may not be present:
Example: Optional Properties
interface Product {
id: number;
name: string;
price: number;
description?: string; // Optional property
category?: string; // Optional property
}
// Valid - optional properties can be omitted
const product1: Product = {
id: 101,
name: "Laptop",
price: 999.99
};
// Also valid - optional properties included
const product2: Product = {
id: 102,
name: "Mouse",
price: 29.99,
description: "Wireless gaming mouse",
category: "Accessories"
};
Readonly Properties
The readonly modifier prevents properties from being modified after object creation:
Example: Readonly Properties
interface Configuration {
readonly apiKey: string;
readonly apiUrl: string;
timeout: number;
}
const config: Configuration = {
apiKey: "abc123xyz",
apiUrl: "https://api.example.com",
timeout: 5000
};
// Valid - timeout is not readonly
config.timeout = 10000;
// Error - cannot modify readonly property
config.apiKey = "newkey123"; // Error!
// Readonly arrays
interface DataSet {
readonly values: readonly number[];
}
const data: DataSet = {
values: [1, 2, 3, 4, 5]
};
// Error - cannot modify readonly array
data.values.push(6); // Error!
Function Type Interfaces
Interfaces can describe function signatures:
Example: Function Interfaces
// Interface for a function type
interface MathOperation {
(a: number, b: number): number;
}
const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;
// Interface with multiple methods
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
multiply(a: number, b: number): number;
divide(a: number, b: number): number;
}
const calculator: Calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
Interface Extension (Inheritance)
Interfaces can extend other interfaces using the extends keyword:
Example: Extending Interfaces
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
department: string;
salary: number;
}
const employee: Employee = {
name: "John Doe",
age: 30,
employeeId: 12345,
department: "Engineering",
salary: 75000
};
// Multiple interface extension
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface AuditableEmployee extends Employee, Timestamped {
lastModifiedBy: string;
}
const auditableEmployee: AuditableEmployee = {
name: "Jane Smith",
age: 28,
employeeId: 67890,
department: "Marketing",
salary: 68000,
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-06-15"),
lastModifiedBy: "admin"
};
Index Signatures
Allow interfaces to have properties with dynamic keys:
Example: Index Signatures
// String index signature
interface StringMap {
[key: string]: string;
}
const translations: StringMap = {
hello: "Hola",
goodbye: "Adiós",
thanks: "Gracias"
};
// Number index signature
interface NumberArray {
[index: number]: number;
}
const fibonacci: NumberArray = [0, 1, 1, 2, 3, 5, 8, 13];
// Mixed with known properties
interface UserDatabase {
[userId: string]: User;
count: number; // Known property must match index signature type
}
const userDb: UserDatabase = {
"user_001": { id: 1, name: "Alice", email: "alice@example.com", age: 28 },
"user_002": { id: 2, name: "Bob", email: "bob@example.com", age: 32 },
count: 2
};
Interface Merging (Declaration Merging)
💡 Did you know? TypeScript automatically merges multiple interface declarations with the same name. This is called declaration merging and is unique to interfaces.
Example: Interface Merging
// First declaration
interface Settings {
theme: string;
language: string;
}
// Second declaration - automatically merged
interface Settings {
fontSize: number;
notifications: boolean;
}
// The merged interface has all properties
const appSettings: Settings = {
theme: "dark",
language: "en",
fontSize: 14,
notifications: true
};
// Useful for extending library types
interface Window {
myCustomProperty: string;
}
window.myCustomProperty = "Hello World";
Implementing Interfaces in Classes
Classes can implement interfaces using the implements keyword:
Example: Class Implementation
interface Vehicle {
brand: string;
model: string;
year: number;
start(): void;
stop(): void;
}
class Car implements Vehicle {
brand: string;
model: string;
year: number;
constructor(brand: string, model: string, year: number) {
this.brand = brand;
this.model = model;
this.year = year;
}
start(): void {
console.log(`${this.brand} ${this.model} is starting...`);
}
stop(): void {
console.log(`${this.brand} ${this.model} is stopping...`);
}
}
const myCar = new Car("Toyota", "Camry", 2024);
myCar.start(); // Output: Toyota Camry is starting...
Hybrid Types
Interfaces can describe objects that act as both a function and an object with properties:
Example: Hybrid Types
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function createCounter(): Counter {
const counter = function(start: number) {
return `Counter started at ${start}`;
} as Counter;
counter.interval = 1000;
counter.reset = function() {
console.log("Counter reset");
};
return counter;
}
const myCounter = createCounter();
console.log(myCounter(0)); // Counter started at 0
console.log(myCounter.interval); // 1000
myCounter.reset(); // Counter reset
Interface vs Type Alias
| Feature | Interface | Type Alias |
|---|---|---|
| Declaration Merging | ✅ Supported | ❌ Not Supported |
| Extends Classes | ✅ Yes | ❌ No |
| Union Types | ❌ No | ✅ Yes |
| Intersection Types | ❌ No (use extends) | ✅ Yes |
| Primitives & Tuples | ❌ No | ✅ Yes |
| Performance | Slightly better for objects | Same for most cases |
Best Practices
- Naming Convention: Use PascalCase and descriptive names (e.g.,
UserProfile,ApiResponse) - Prefer Interfaces for Objects: Use interfaces when defining object shapes; use type aliases for unions and primitives
- Keep Interfaces Focused: Follow the Single Responsibility Principle—each interface should represent one concept
- Use Optional Properties Wisely: Don't make everything optional; be intentional about what's required
- Document Complex Interfaces: Add JSDoc comments for interfaces with non-obvious purposes
- Avoid Empty Interfaces: Empty interfaces provide no type safety; use
{}orRecord<string, unknown>instead
Common Pitfalls
⚠️ Watch Out: Remember that interfaces are compile-time only. They don't exist in runtime JavaScript, so you can't use instanceof to check if an object matches an interface. Use type guards or runtime validation libraries instead.
Practice Exercise
Try creating these interfaces to practice what you've learned:
- Create a
BlogPostinterface with title, content, author, publishedDate, and optional tags array - Extend it to create a
FeaturedPostinterface that adds featuredImage and priority properties - Create a
Repository<T>interface with methods:getAll(),getById(id: number),create(item: T),update(id: number, item: T),delete(id: number) - Implement the
Repositoryinterface in aBlogPostRepositoryclass - Create an interface with an index signature to represent a dictionary of products where keys are SKU codes
Key Takeaways
- Interfaces define contracts for object shapes and provide strong type safety
- Use
?for optional properties andreadonlyfor immutable properties - Interfaces can extend other interfaces for code reusability
- Declaration merging allows multiple interface declarations with the same name to be automatically merged
- Classes can implement interfaces using the
implementskeyword - Index signatures enable dynamic property names with type constraints
- Prefer interfaces for object shapes; use type aliases for unions and primitives
- Interfaces exist only at compile-time and generate no runtime JavaScript code
What's Next?
Next Step: Now that you understand interfaces, let's explore Classes in TypeScript. You'll learn how to combine interfaces with classes to create robust, object-oriented code with full type safety and powerful inheritance capabilities.
TypeScript Classes: Object-Oriented Programming
Master class syntax, access modifiers, inheritance, and object-oriented design patterns in TypeScript
What are Classes in TypeScript?
Classes are blueprints for creating objects with predefined properties and methods. TypeScript enhances JavaScript's class syntax with static typing, access modifiers, abstract classes, and more powerful object-oriented programming features. Classes compile to constructor functions in JavaScript for backward compatibility.
Classes are fundamental to object-oriented programming (OOP) and help you organize code into reusable, maintainable structures. TypeScript's class system builds upon ES6 classes while adding type safety and additional features that make your code more robust and self-documenting.
Why Use Classes?
Encapsulation
Bundle data and methods that operate on that data together, hiding internal implementation details.
Reusability
Create reusable blueprints that can be instantiated multiple times with different data.
Inheritance
Build class hierarchies where child classes inherit and extend functionality from parent classes.
Type Safety
Get compile-time type checking for properties, methods, and method parameters.
Basic Class Syntax
Let's start with a simple class definition:
Example: Basic Class
class Person {
// Properties
name: string;
age: number;
// Constructor
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// Method
greet(): string {
return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
}
}
// Creating instances
const person1 = new Person("Alice", 28);
const person2 = new Person("Bob", 32);
console.log(person1.greet()); // Hello, my name is Alice and I'm 28 years old.
console.log(person2.greet()); // Hello, my name is Bob and I'm 32 years old.
Access Modifiers
TypeScript provides three access modifiers to control the visibility of class members:
| Modifier | Description | Accessible From |
|---|---|---|
public |
Default. Accessible from anywhere | Class, subclasses, and external code |
private |
Only accessible within the class | Only the defining class |
protected |
Accessible in class and subclasses | Class and its subclasses |
Example: Access Modifiers
class BankAccount {
public accountNumber: string; // Accessible everywhere
private balance: number; // Only within this class
protected owner: string; // This class and subclasses
constructor(accountNumber: string, owner: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = initialBalance;
}
// Public method to access private balance
public getBalance(): number {
return this.balance;
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited $${amount}. New balance: $${this.balance}`);
}
}
private validateTransaction(amount: number): boolean {
return amount > 0 && amount <= this.balance;
}
public withdraw(amount: number): boolean {
if (this.validateTransaction(amount)) {
this.balance -= amount;
console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
return true;
}
console.log("Invalid transaction");
return false;
}
}
const account = new BankAccount("123456", "Alice", 1000);
console.log(account.accountNumber); // OK: public
console.log(account.getBalance()); // OK: accessing via public method
// console.log(account.balance); // Error: private
// console.log(account.owner); // Error: protected
Constructor Shorthand
TypeScript provides a shorthand syntax for declaring and initializing properties in the constructor:
Example: Constructor Shorthand
// Long form
class Product {
name: string;
price: number;
category: string;
constructor(name: string, price: number, category: string) {
this.name = name;
this.price = price;
this.category = category;
}
}
// Shorthand - much cleaner!
class ProductShort {
constructor(
public name: string,
public price: number,
public category: string
) {}
}
const product = new ProductShort("Laptop", 999.99, "Electronics");
console.log(product.name); // Laptop
Getters and Setters
Getters and setters allow you to control access to class properties:
Example: Getters and Setters
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
// Getter
get celsius(): number {
return this._celsius;
}
// Setter with validation
set celsius(value: number) {
if (value < -273.15) {
throw new Error("Temperature cannot be below absolute zero");
}
this._celsius = value;
}
// Computed property
get fahrenheit(): number {
return (this._celsius * 9/5) + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
temp.celsius = 30;
console.log(temp.fahrenheit); // 86
temp.fahrenheit = 68;
console.log(temp.celsius); // 20
Readonly Properties
The readonly modifier makes properties immutable after initialization:
Example: Readonly Properties
class User {
readonly id: number;
readonly createdAt: Date;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
this.createdAt = new Date();
}
updateName(newName: string): void {
this.name = newName; // OK
}
}
const user = new User(1, "Alice");
user.name = "Alice Johnson"; // OK
// user.id = 2; // Error: readonly
// user.createdAt = new Date(); // Error: readonly
Inheritance
Classes can inherit from other classes using the extends keyword:
Example: Class Inheritance
class Animal {
constructor(public name: string) {}
move(distance: number = 0): void {
console.log(`${this.name} moved ${distance} meters.`);
}
makeSound(): void {
console.log("Some generic animal sound");
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Call parent constructor
}
// Override parent method
makeSound(): void {
console.log("Woof! Woof!");
}
// New method specific to Dog
fetch(): void {
console.log(`${this.name} is fetching the ball!`);
}
}
class Cat extends Animal {
constructor(name: string) {
super(name);
}
makeSound(): void {
console.log("Meow!");
}
climb(): void {
console.log(`${this.name} climbed a tree!`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.move(10); // Buddy moved 10 meters.
dog.makeSound(); // Woof! Woof!
dog.fetch(); // Buddy is fetching the ball!
const cat = new Cat("Whiskers");
cat.move(5); // Whiskers moved 5 meters.
cat.makeSound(); // Meow!
cat.climb(); // Whiskers climbed a tree!
Static Members
Static properties and methods belong to the class itself rather than instances:
Example: Static Members
class MathUtils {
static PI: number = 3.14159;
static E: number = 2.71828;
static circleArea(radius: number): number {
return this.PI * radius * radius;
}
static circleCircumference(radius: number): number {
return 2 * this.PI * radius;
}
}
// Access static members without creating instance
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.circleArea(5)); // 78.53975
console.log(MathUtils.circleCircumference(5)); // 31.4159
class Counter {
static count: number = 0;
constructor() {
Counter.count++;
}
static getCount(): number {
return Counter.count;
}
}
new Counter();
new Counter();
new Counter();
console.log(Counter.getCount()); // 3
Abstract Classes
Abstract classes are base classes that cannot be instantiated directly. They define a contract for subclasses:
Example: Abstract Classes
abstract class Shape {
constructor(public color: string) {}
// Abstract method - must be implemented by subclasses
abstract calculateArea(): number;
abstract calculatePerimeter(): number;
// Concrete method - inherited by subclasses
describe(): string {
return `A ${this.color} shape with area ${this.calculateArea()}`;
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(color: string, public width: number, public height: number) {
super(color);
}
calculateArea(): number {
return this.width * this.height;
}
calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
}
// const shape = new Shape("red"); // Error: Cannot instantiate abstract class
const circle = new Circle("blue", 5);
console.log(circle.describe()); // A blue shape with area 78.54...
console.log(circle.calculatePerimeter()); // 31.41...
const rectangle = new Rectangle("green", 4, 6);
console.log(rectangle.describe()); // A green shape with area 24
console.log(rectangle.calculatePerimeter()); // 20
Implementing Interfaces
Classes can implement one or more interfaces:
Example: Implementing Interfaces
interface Flyable {
fly(): void;
altitude: number;
}
interface Swimmable {
swim(): void;
depth: number;
}
class Duck implements Flyable, Swimmable {
altitude: number = 0;
depth: number = 0;
constructor(public name: string) {}
fly(): void {
this.altitude = 100;
console.log(`${this.name} is flying at ${this.altitude} feet`);
}
swim(): void {
this.depth = 5;
console.log(`${this.name} is swimming at ${this.depth} feet deep`);
}
quack(): void {
console.log("Quack! Quack!");
}
}
const duck = new Duck("Donald");
duck.fly(); // Donald is flying at 100 feet
duck.swim(); // Donald is swimming at 5 feet deep
duck.quack(); // Quack! Quack!
Class Expressions
Classes can also be defined as expressions:
Example: Class Expressions
const MyClass = class {
constructor(public value: number) {}
multiply(n: number): number {
return this.value * n;
}
};
const instance = new MyClass(5);
console.log(instance.multiply(3)); // 15
// Anonymous class
const createPoint = () => {
return new class {
constructor(public x: number, public y: number) {}
distance(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}(3, 4);
};
const point = createPoint();
console.log(point.distance()); // 5
Parameter Properties
💡 Pro Tip: You can declare and initialize class properties directly in the constructor parameters. This is called parameter properties and is a TypeScript-specific feature that reduces boilerplate code.
Example: Parameter Properties with All Modifiers
class Employee {
// All properties declared and initialized in constructor
constructor(
public readonly id: number,
public name: string,
private salary: number,
protected department: string
) {}
getSalary(): number {
return this.salary;
}
raiseSalary(percentage: number): void {
this.salary *= (1 + percentage / 100);
}
}
const emp = new Employee(1, "John Doe", 50000, "Engineering");
console.log(emp.id); // 1
console.log(emp.name); // John Doe
// console.log(emp.salary); // Error: private
Best Practices
- Single Responsibility: Each class should have one clear purpose and responsibility
- Encapsulation: Use private/protected modifiers to hide implementation details
- Favor Composition Over Inheritance: Don't overuse inheritance; consider composition for flexibility
- Use Access Modifiers: Be explicit about public, private, and protected members
- Initialize in Constructor: Always initialize properties, or mark them as optional
- Immutability: Use
readonlyfor properties that shouldn't change after construction - Abstract Base Classes: Use abstract classes to define contracts for related subclasses
- Interface Implementation: Implement interfaces to ensure classes conform to contracts
Common Patterns
Example: Singleton Pattern
class Database {
private static instance: Database;
private constructor() {
console.log("Database instance created");
}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
query(sql: string): void {
console.log(`Executing: ${sql}`);
}
}
// const db = new Database(); // Error: constructor is private
const db1 = Database.getInstance(); // Database instance created
const db2 = Database.getInstance(); // Uses existing instance
console.log(db1 === db2); // true
Practice Exercise
Build a library management system with the following requirements:
- Create an abstract
LibraryItemclass with properties: id, title, and year. Include an abstract methodgetDescription() - Create
BookandMagazineclasses that extendLibraryItem - Add a
Borrowableinterface with methods:borrow(),return(), and propertyisAvailable - Implement the
Borrowableinterface in your classes - Create a
Libraryclass with static property for tracking total items and methods to add/remove items - Use appropriate access modifiers, getters/setters, and readonly properties
Key Takeaways
- Classes are blueprints for creating objects with properties and methods
- Access modifiers (public, private, protected) control visibility of class members
- Constructor shorthand allows declaring and initializing properties in one place
- Getters and setters provide controlled access to private properties
- Classes can inherit from other classes using
extends - Static members belong to the class itself, not instances
- Abstract classes define contracts that subclasses must implement
- Classes can implement one or more interfaces
- Use
readonlyto prevent property modification after initialization
What's Next?
Next Step: Now that you've mastered classes, let's explore Functions in TypeScript. You'll learn about function types, optional and default parameters, rest parameters, overloading, and arrow functions with full type safety.
TypeScript Functions: Types, Parameters, and Overloads
Learn to type parameters, returns, overloads, and callbacks while leveraging inference and safety
Why Type Functions?
Functions are contracts. Typing parameters and return values catches mistakes early, improves IntelliSense, and documents intent. Always type parameters; return types can often be inferred but should be explicit on public APIs.
Typed Parameters and Returns
function add(a: number, b: number): number {
return a + b;
}
// Inference works too, but return type can be explicit for APIs
const multiply = (a: number, b: number): number => a * b;
// Void for functions that don't return
function logMessage(message: string): void {
console.log(message);
}
// Never for functions that never complete
function fail(message: string): never {
throw new Error(message);
}
Optional and Default Parameters
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
function formatPrice(amount: number, currency: string = 'USD'): string {
return `${currency} ${amount.toFixed(2)}`;
}
greet('Alice'); // "Alice"
greet('Alice', 'Dr.'); // "Dr. Alice"
formatPrice(9.5); // "USD 9.50"
formatPrice(9.5, 'EUR'); // "EUR 9.50"
Rest Parameters
function sum(...values: number[]): number {
return values.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 5); // 65
Function Types and Callbacks
// Function type alias
type Transformer = (input: string) => string;
const toUpper: Transformer = (text) => text.toUpperCase();
const trim: Transformer = (text) => text.trim();
// Passing callbacks
function process(text: string, transform: Transformer): string {
return transform(text);
}
process(' hello ', trim); // "hello"
process('typescript', toUpper); // "TYPESCRIPT"
Function Overloads
// Overload signatures
function format(input: number): string;
function format(input: Date): string;
function format(input: string): string;
// Implementation signature
function format(input: number | Date | string): string {
if (typeof input === 'number') return input.toFixed(2);
if (input instanceof Date) return input.toISOString();
return input.trim();
}
format(12.345); // "12.35"
format(new Date()); // ISO string
format(' hello '); // "hello"
Rule: Overload signatures come first; the implementation uses the union of allowed types and performs runtime narrowing.
this Parameters
interface Button {
label: string;
click(this: Button): void;
}
const button: Button = {
label: 'Save',
click() {
console.log(`Clicked ${this.label}`);
},
};
// Arrow functions capture this lexically, so rarely need explicit this
Generic Functions
function wrap(value: T): { value: T } {
return { value };
}
const wrappedNumber = wrap(42); // { value: number }
const wrappedUser = wrap({ name: 'A' }); // { value: { name: string } }
Avoid quick fixes: Skip @ts-ignore and any. Prefer unions, generics, or type guards to maintain safety.
Practice Tasks
- Create a function with optional and default parameters (e.g., greet).
- Write a function type alias and use it to type a callback.
- Implement function overloads for parsing numbers, dates, and strings.
- Add a generic function that returns the first element of an array.
- Type a method that uses an explicit
thisparameter. - Refactor a function to remove all
anytypes using unions or generics.
Key Takeaways
- Type all parameters; annotate return types on public APIs.
- Use optional (?), defaults, and rest parameters for flexibility.
- Overloads document multiple call signatures; implement with unions and narrowing.
- Arrow functions inherit
this; add explicitthistyping when needed. - Prefer generics and guards over
anyand assertions.
What's Next?
Next Topic: Explore Union and Intersection Types to combine shapes safely.
TypeScript Generics: Writing Reusable, Type-Safe Code
Master generics to create flexible, reusable functions and classes that work with any data type while maintaining type safety
What are Generics?
Generics allow you to write code that works with multiple types while preserving type information. Instead of using any, generics let you create reusable components that maintain type safety. Think of generics as "type variables" or "type parameters" that are filled in when the code is used.
The Problem Without Generics
// Without generics - loses type safety
function identity(value: any): any {
return value;
}
const result1 = identity("hello"); // result1 is any
const result2 = identity(42); // result2 is any
// No autocomplete, no type checking
result1.toUpperCase(); // Works
result2.toUpperCase(); // No error, but crashes at runtime!
Basic Generic Functions
// Generic function with type parameter T
function identity(value: T): T {
return value;
}
// TypeScript infers the type
const str = identity("hello"); // str: string
const num = identity(42); // num: number
const bool = identity(true); // bool: boolean
// Explicitly specify the type
const result = identity("hello");
// Now we have full type safety
str.toUpperCase(); // ✅ Works - TypeScript knows it's a string
num.toUpperCase(); // ❌ Error - Property 'toUpperCase' does not exist on type 'number'
Generic Arrays and Functions
// Get first element of array
function first(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const firstNum = first(numbers); // type: number | undefined
const names = ["Alice", "Bob"];
const firstName = first(names); // type: string | undefined
// Get last element
function last(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
// Filter array
function filter(arr: T[], predicate: (item: T) => boolean): T[] {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
const even = filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0);
// even: number[] = [2, 4, 6]
Generic Interfaces
// Generic interface for API response
interface ApiResponse {
data: T;
status: number;
message: string;
}
// Use with different data types
interface User {
id: number;
name: string;
}
const userResponse: ApiResponse = {
data: { id: 1, name: "Alice" },
status: 200,
message: "Success"
};
const numbersResponse: ApiResponse = {
data: [1, 2, 3, 4, 5],
status: 200,
message: "Success"
};
// Generic interface for key-value pairs
interface KeyValuePair {
key: K;
value: V;
}
const pair1: KeyValuePair = {
key: "age",
value: 25
};
const pair2: KeyValuePair = {
key: 1,
value: "First"
};
Generic Classes
// Generic Stack data structure
class Stack {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// Use with numbers
const numberStack = new Stack();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop()); // 3
// Use with strings
const stringStack = new Stack();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.pop()); // "world"
// Type safety enforced
// numberStack.push("string"); // ❌ Error: Argument of type 'string' not assignable to parameter of type 'number'
Generic Constraints
// Constraint: T must have a length property
function getLength(item: T): number {
return item.length;
}
getLength("hello"); // ✅ OK - strings have length
getLength([1, 2, 3]); // ✅ OK - arrays have length
getLength({ length: 10 }); // ✅ OK - object has length
getLength(42); // ❌ Error: number doesn't have length
// Constraint with interface
interface Printable {
print(): void;
}
function printItem(item: T): void {
item.print();
}
// Multiple constraints
function merge(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: "Alice" }, { age: 25 });
// merged: { name: string } & { age: number }
console.log(merged.name); // "Alice"
console.log(merged.age); // 25
Multiple Type Parameters
// Function with two type parameters
function pair(first: T, second: U): [T, U] {
return [first, second];
}
const result1 = pair("age", 25); // [string, number]
const result2 = pair(true, "yes"); // [boolean, string]
// Generic Map-like structure
class Dictionary {
private items: Map = new Map();
set(key: K, value: V): void {
this.items.set(key, value);
}
get(key: K): V | undefined {
return this.items.get(key);
}
has(key: K): boolean {
return this.items.has(key);
}
entries(): [K, V][] {
return Array.from(this.items.entries());
}
}
const userAges = new Dictionary();
userAges.set("Alice", 25);
userAges.set("Bob", 30);
console.log(userAges.get("Alice")); // 25
Default Type Parameters
// Generic with default type
interface Box {
value: T;
}
// Uses default (string)
const box1: Box = { value: "hello" };
// Explicitly specify different type
const box2: Box = { value: 42 };
// Function with default type parameter
function create(value?: T): T {
return value ?? { id: Date.now() } as T;
}
const obj1 = create(); // type: { id: number }
const obj2 = create({ name: "Alice" }); // type: { name: string }
Real-World Example: HTTP Client
// Generic HTTP client
class HttpClient {
async get(url: string): Promise {
const response = await fetch(url);
return response.json() as Promise;
}
async post(url: string, data: T): Promise {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
});
return response.json() as Promise;
}
}
// Define data types
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
// Use with type safety
const client = new HttpClient();
// GET request - response typed as User[]
const users = await client.get('/api/users');
users.forEach(user => console.log(user.name)); // Full autocomplete
// POST request - request and response typed
const newUser = await client.post(
'/api/users',
{ name: "Alice", email: "alice@example.com" }
);
console.log(newUser.id); // Full type safety
Practice Tasks
- Task 1: Create a generic function that swaps two values in a tuple.
- Task 2: Implement a generic Queue class with enqueue and dequeue methods.
- Task 3: Create a generic function to find an item in an array by property.
- Task 4: Build a generic Result type for error handling (success/failure).
- Task 5: Implement a generic cache with get/set/has methods.
- Task 6: Create a generic debounce function with proper typing.
- Task 7: Build a generic form state manager with validation.
Key Takeaways
- Generics preserve type information: Better than using any
- Type parameters use angle brackets: <T> convention
- TypeScript infers types: Usually don't need to specify explicitly
- Constraints narrow possibilities: Use extends for requirements
- Multiple type parameters: <T, U, V> for complex cases
- Default type parameters: Provide fallback types
- Works with functions, classes, interfaces: Universal feature
What's Next?
Next Topic: Learn about Enums in TypeScript - a way to define named constants with better type safety than plain objects or strings.
TypeScript Enums: Named Constants for Better Code
Learn to use enums for creating sets of named constants that improve code readability and type safety
What are Enums?
Enums (enumerations) allow you to define a set of named constants. They provide a way to give friendly names to sets of numeric or string values, making your code more readable and self-documenting. Unlike most TypeScript features, enums produce real JavaScript code at runtime.
Numeric Enums
// Numeric enum - auto-increments from 0
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// Usage
let direction: Direction = Direction.Up;
console.log(direction); // 0
if (direction === Direction.Up) {
console.log("Moving up!");
}
// With custom starting value
enum Status {
Pending = 1,
Approved, // 2
Rejected, // 3
Completed // 4
}
console.log(Status.Pending); // 1
console.log(Status.Approved); // 2
String Enums
// String enum - must initialize all members
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
console.log(Color.Red); // "RED"
// More descriptive string values
enum LogLevel {
Error = "ERROR",
Warning = "WARNING",
Info = "INFO",
Debug = "DEBUG"
}
function log(message: string, level: LogLevel): void {
console.log(`[${level}] ${message}`);
}
log("Application started", LogLevel.Info);
// Output: [INFO] Application started
// String enums are great for API responses
enum UserRole {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER",
Guest = "GUEST"
}
function checkPermission(role: UserRole): boolean {
return role === UserRole.Admin || role === UserRole.Moderator;
}
Heterogeneous Enums
// Mixing string and number values (not recommended)
enum BooleanLike {
No = 0,
Yes = "YES"
}
// Better approach - use separate enums
enum NumericBoolean {
False = 0,
True = 1
}
enum StringBoolean {
False = "FALSE",
True = "TRUE"
}
Computed and Constant Members
// Constant enum members
enum FileAccess {
None,
Read = 1 << 1, // Computed: 2
Write = 1 << 2, // Computed: 4
ReadWrite = Read | Write, // Computed: 6
All = ReadWrite | 1 // Computed: 7
}
console.log(FileAccess.Read); // 2
console.log(FileAccess.Write); // 4
console.log(FileAccess.ReadWrite); // 6
// Check permissions using bitwise operations
function hasPermission(access: FileAccess, permission: FileAccess): boolean {
return (access & permission) === permission;
}
const userAccess = FileAccess.ReadWrite;
console.log(hasPermission(userAccess, FileAccess.Read)); // true
console.log(hasPermission(userAccess, FileAccess.Write)); // true
Reverse Mapping
// Numeric enums support reverse mapping
enum Status {
Active = 1,
Inactive = 2,
Pending = 3
}
// Forward mapping: name -> value
console.log(Status.Active); // 1
// Reverse mapping: value -> name
console.log(Status[1]); // "Active"
console.log(Status[2]); // "Inactive"
// Iterate over enum
function printEnumValues(enumObj: any): void {
for (const key in enumObj) {
if (isNaN(Number(key))) {
console.log(`${key} = ${enumObj[key]}`);
}
}
}
printEnumValues(Status);
// Output:
// Active = 1
// Inactive = 2
// Pending = 3
// Note: String enums do NOT have reverse mapping
enum Color {
Red = "RED"
}
console.log(Color["RED"]); // undefined
Const Enums
// Regular enum - creates JavaScript object
enum RegularEnum {
A,
B,
C
}
const value1 = RegularEnum.A; // Compiles to: const value1 = RegularEnum.A;
// Const enum - inlined at compile time
const enum ConstEnum {
A,
B,
C
}
const value2 = ConstEnum.A; // Compiles to: const value2 = 0;
// Advantages of const enums:
// - No runtime object created
// - Values are inlined
// - Smaller bundle size
// - Better performance
// Disadvantage:
// - Cannot use reverse mapping
// - Cannot iterate over values
Enums vs Union Types
| Feature | Enum | Union Type |
|---|---|---|
| Runtime representation | ✅ Yes - real object | ❌ No - compile-time only |
| Autocomplete | ✅ Excellent | ✅ Excellent |
| Reverse mapping | ✅ Yes (numeric only) | ❌ No |
| Bundle size | ⚠️ Larger | ✅ Zero runtime cost |
| Iteration | ✅ Possible | ❌ Not possible |
| API integration | ✅ Direct mapping | ⚠️ Need conversion |
// Using enum
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
function paintWithEnum(color: Color): void {
console.log(`Painting with ${color}`);
}
paintWithEnum(Color.Red); // ✅ OK
// Using union type
type ColorUnion = "RED" | "GREEN" | "BLUE";
function paintWithUnion(color: ColorUnion): void {
console.log(`Painting with ${color}`);
}
paintWithUnion("RED"); // ✅ OK
// Enum provides namespace
Color.Red; // Clear where it comes from
// Union type requires quotes
"RED"; // Less clear, could be any string
Real-World Examples
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500
}
function handleResponse(status: HttpStatus): string {
switch (status) {
case HttpStatus.OK:
return "Success!";
case HttpStatus.Created:
return "Resource created!";
case HttpStatus.BadRequest:
return "Invalid request";
case HttpStatus.Unauthorized:
return "Please login";
case HttpStatus.NotFound:
return "Resource not found";
default:
return "An error occurred";
}
}
console.log(handleResponse(HttpStatus.OK)); // "Success!"
enum DayOfWeek {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
function isWeekend(day: DayOfWeek): boolean {
return day === DayOfWeek.Saturday || day === DayOfWeek.Sunday;
}
function getWorkingHours(day: DayOfWeek): string {
if (isWeekend(day)) {
return "Closed";
}
return "9 AM - 5 PM";
}
console.log(getWorkingHours(DayOfWeek.Monday)); // "9 AM - 5 PM"
console.log(getWorkingHours(DayOfWeek.Saturday)); // "Closed"
Practice Tasks
- Task 1: Create an enum for months of the year with numeric values.
- Task 2: Define a string enum for user roles in your application.
- Task 3: Build an enum for file types with appropriate string values.
- Task 4: Create a const enum for common colors to optimize bundle size.
- Task 5: Implement an enum for card suits in a card game.
- Task 6: Use bitwise enum for file permissions (read/write/execute).
- Task 7: Create an enum for HTTP methods (GET, POST, PUT, DELETE).
Key Takeaways
- Enums create named constants: Better than magic numbers/strings
- Numeric enums auto-increment: Start from 0 by default
- String enums require explicit values: Must initialize all members
- Reverse mapping: Only for numeric enums
- Const enums: Inlined for better performance
- Use enums for fixed sets of values: HTTP status, days, roles
- Consider union types: When runtime object not needed
What's Next?
Next Topic: Learn about Type Assertions - how to tell TypeScript "I know better than you" about a value's type when you have more information than the compiler.
TypeScript Type Assertions: Overriding Type Inference
Learn when and how to use type assertions to tell TypeScript the specific type of a value
What are Type Assertions?
Type assertions are a way to tell the TypeScript compiler "trust me, I know what I'm doing." They let you override the compiler's inferred or assigned type when you have more specific information. Type assertions don't change the runtime behavior—they're purely a compile-time construct.
Basic Syntax: as Keyword
// DOM manipulation - common use case
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test@example.com"; // ✅ OK - TypeScript knows it has value property
// Without assertion
const input2 = document.getElementById("email"); // type: HTMLElement | null
// input2.value = "test"; // ❌ Error: Property 'value' does not exist
// API response with unknown type
const response: unknown = await fetch("/api/data").then(r => r.json());
const data = response as { id: number; name: string };
console.log(data.name); // ✅ OK
// String to specific format
const userId: any = "12345";
const id = userId as string;
const numericId = parseInt(id);
Angle-Bracket Syntax
// Angle-bracket syntax - older style
const input = document.getElementById("email");
input.value = "test@example.com";
// Note: This syntax conflicts with JSX/TSX
// Always use 'as' syntax in React files
// Multiple assertions
const value = someValue;
⚠️ JSX Conflict: Use the as keyword in .tsx files because angle brackets are used for JSX elements. The as syntax is now the recommended approach everywhere.
Common Use Cases
// Specific element types
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const context = canvas.getContext("2d");
const video = document.querySelector("video") as HTMLVideoElement;
video.play();
const form = document.forms[0] as HTMLFormElement;
form.submit();
// Query selector with assertions
const button = document.querySelector(".submit-btn") as HTMLButtonElement;
button.disabled = false;
// Multiple element assertions
const inputs = document.querySelectorAll("input") as NodeListOf;
inputs.forEach(input => {
console.log(input.value);
});
Type Assertions with Objects
// Define interface
interface User {
id: number;
name: string;
email: string;
}
// Assertion from generic object
const userData: object = {
id: 1,
name: "Alice",
email: "alice@example.com"
};
const user = userData as User;
console.log(user.name); // ✅ OK
// Partial object assertion
const partialUser = {
name: "Bob"
} as User; // ⚠️ Compiles but missing required properties!
// Better: Use Partial utility type
const partialUser2: Partial = {
name: "Bob"
};
Const Assertions
// Without const assertion
const colors1 = ["red", "green", "blue"];
// type: string[]
// With const assertion
const colors2 = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]
colors1[0] = "yellow"; // ✅ OK
colors2[0] = "yellow"; // ❌ Error: Cannot assign to readonly
// Object with const assertion
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
} as const;
// All properties become readonly
// config.timeout = 10000; // ❌ Error
// Useful for literal types
type Method = typeof methods[number];
const methods = ["GET", "POST", "PUT", "DELETE"] as const;
function request(method: Method) {
// method can only be one of the array values
}
Double Assertions
// Sometimes TypeScript won't allow direct assertion
const num = 42;
// const str = num as string; // ❌ Error: Conversion may be a mistake
// Double assertion via unknown or any
const str = num as unknown as string; // ⚠️ Compiles but dangerous!
// Better approach: actual conversion
const str2 = String(num); // ✅ Safe runtime conversion
// Valid use case: complex type transformations
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
const dog: Dog = {
bark: () => console.log("Woof!")
};
// Force incompatible types (rarely needed)
const cat = dog as unknown as Cat; // ⚠️ Runtime error if you call cat.meow()
Non-Null Assertions
// Non-null assertion operator !
const element = document.getElementById("myElement")!;
// Tells TypeScript: "I guarantee this is not null"
// Without ! operator
const element2 = document.getElementById("myElement");
// Type: HTMLElement | null
// element2.classList.add("active"); // ❌ Error
// With ! operator
element.classList.add("active"); // ✅ OK
// Array access
const numbers = [1, 2, 3];
const first = numbers[0]!; // Asserts value exists
// Optional chaining vs non-null assertion
const user = getUser();
const name1 = user?.name; // Safe: string | undefined
const name2 = user!.name; // Dangerous: assumes user exists
⚠️ Danger Zone: Non-null assertions bypass TypeScript's safety checks. Only use them when you're absolutely certain a value exists. Runtime errors can still occur!
Type Guards vs Assertions
| Feature | Type Assertion | Type Guard |
|---|---|---|
| Runtime safety | ❌ No checking | ✅ Validates at runtime |
| Compile-time only | ✅ Yes | ❌ Runs in JavaScript |
| Type narrowing | ⚠️ Forces type | ✅ Safe narrowing |
| Use case | When you know better | When you need to check |
// Type assertion - no runtime check
function processInput(input: unknown) {
const str = input as string;
console.log(str.toUpperCase()); // ⚠️ Crashes if input isn't string
}
// Type guard - safe runtime check
function processInputSafe(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase()); // ✅ Safe
}
}
// User-defined type guard
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows it's Fish
} else {
pet.fly(); // TypeScript knows it's Bird
}
}
Best Practices
- ✅ DO: Use for DOM elements when you know the specific type
- ✅ DO: Use with third-party libraries that lack type definitions
- ✅ DO: Use as const for literal types
- ✅ DO: Use when migrating JavaScript to TypeScript gradually
- ❌ DON'T: Use to bypass type errors you should fix
- ❌ DON'T: Use instead of proper type guards for validation
- ❌ DON'T: Overuse non-null assertions (!)
- ❌ DON'T: Use double assertions unless absolutely necessary
Practice Tasks
- Task 1: Get a canvas element and assert it as HTMLCanvasElement.
- Task 2: Use as const to create a readonly configuration object.
- Task 3: Assert an API response to a specific interface type.
- Task 4: Practice non-null assertions with array access.
- Task 5: Compare type assertion vs type guard approaches.
- Task 6: Create a helper function that safely asserts types.
- Task 7: Use querySelectorAll with proper type assertion.
Key Takeaways
- Type assertions override compiler inference: Use sparingly
- as syntax recommended: Works everywhere, including JSX
- Angle brackets conflict with JSX: Avoid in .tsx files
- as const creates readonly: Useful for literal types
- Non-null assertion (!) is dangerous: Only when absolutely certain
- Prefer type guards: Safer than assertions for validation
- No runtime effect: Assertions are compile-time only
What's Next?
Next Topic: Learn about Union and Intersection Types - powerful ways to combine multiple types for flexible yet type-safe code.
Union and Intersection Types: Composing Flexible Contracts
Combine types safely to model real-world data with confidence
Why Use Union and Intersection Types?
Unions model “this or that” scenarios (A | B), while intersections model “this and that” (A & B). Together they let you express flexible yet type-safe data shapes.
Union Types (|)
type Status = 'pending' | 'approved' | 'rejected';
function setStatus(status: Status) {
// autocomplete only allows the three literals
}
setStatus('approved'); // ✅
// setStatus('done'); // ❌ Error
// Value can be number or string
function toId(id: number | string): string {
return typeof id === 'number' ? id.toString() : id;
}
Type Narrowing with Unions
function format(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // value is string here
}
return value.toFixed(2); // value is number here
}
// Discriminated union
type Success = { type: 'success'; data: string };
type Failure = { type: 'error'; message: string };
type Result = Success | Failure;
function handle(result: Result) {
if (result.type === 'success') {
console.log(result.data);
} else {
console.error(result.message);
}
}
Intersection Types (&)
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type User = { id: number; name: string };
type TimestampedUser = User & WithTimestamps;
const alice: TimestampedUser = {
id: 1,
name: 'Alice',
createdAt: new Date(),
updatedAt: new Date(),
};
Intersection caveat: When properties collide with incompatible types, the result becomes never. Ensure overlapping keys are compatible.
Unions vs Intersections at a Glance
| Concept | Union (|) | Intersection (&) |
|---|---|---|
| Meaning | Either A or B | Both A and B |
| Flexibility | More flexible | More strict |
| Use for | Multiple variants | Combining features |
| Narrowing | Required at runtime | No narrowing needed |
Real-World Example: Payment Methods
type CardPayment = {
method: 'card';
cardNumber: string;
cvv: string;
};
type UpiPayment = {
method: 'upi';
upiId: string;
};
type CashPayment = {
method: 'cash';
};
type Payment = CardPayment | UpiPayment | CashPayment;
function processPayment(p: Payment) {
switch (p.method) {
case 'card':
return `Charging card ${p.cardNumber}`;
case 'upi':
return `Requesting UPI ${p.upiId}`;
case 'cash':
return 'Collecting cash';
}
}
Mixing Both: Smart Composition
type Base = { id: string; createdAt: Date };
type Article = Base & { kind: 'article'; title: string; body: string };
type Video = Base & { kind: 'video'; title: string; duration: number };
type Content = Article | Video;
Practice Tasks
- Create a discriminated union for API responses: success, validationError, serverError.
- Model a Vehicle that can be Car | Bike | Truck and narrow by a type field.
- Build an intersection type that adds audit fields (createdBy, updatedBy) to an existing entity.
- Experiment with incompatible intersections to see how conflicts become
never.
Key Takeaways
- Use unions for “one of many” variants; narrow with runtime checks.
- Use intersections to combine capabilities; avoid conflicting property types.
- Discriminated unions (tagged with
typefield) make narrowing simple and safe. - Watch for incompatible intersections that collapse to
never.
What's Next?
Next Topic: Dive into Type Guards to master runtime narrowing.
Type Guards: Runtime Type Checking
Narrow types with confidence using guards and predicates
What Are Type Guards?
Type guards are code patterns that narrow a union type to a more specific type. They use runtime checks to guarantee type safety.
Built-in Type Guards
function printLength(x: string | number) {
if (typeof x === 'string') {
console.log(x.length); // x is string
} else {
console.log(x.toFixed(2)); // x is number
}
}
// Works with: string, number, boolean, symbol, undefined, object, function
class Dog {
bark() { return 'Woof!'; }
}
class Cat {
meow() { return 'Meow!'; }
}
function animalSound(animal: Dog | Cat): string {
if (animal instanceof Dog) {
return animal.bark();
}
return animal.meow();
}
interface Admin {
name: string;
role: 'admin';
}
interface User {
name: string;
permissions: string[];
}
function checkAccess(user: Admin | User) {
if ('role' in user) {
console.log(`Admin: ${user.role}`);
} else {
console.log(`User with ${user.permissions.length} permissions`);
}
}
Custom Type Predicates
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFish(animal: Fish | Bird): animal is Fish {
return 'swim' in animal;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim(); // type is narrowed to Fish
} else {
animal.fly(); // type is narrowed to Bird
}
}
Exhaustiveness Checking
type Status = 'pending' | 'completed' | 'failed';
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Processing...';
case 'completed':
return 'Done!';
case 'failed':
return 'Error occurred';
default:
// Compiler error if Status is extended without handling
const _exhaustive: never = status;
return _exhaustive;
}
}
Type Narrowing with Truthiness
function printValue(x: string | null | undefined) {
if (x) {
console.log(x.toUpperCase()); // x is string
} else {
console.log('No value provided');
}
}
function validateEmail(email: string | null) {
if (!email) return false;
return email.includes('@'); // email is string
}
Real-World Pattern: API Response Handler
type ApiResponse =
| { status: 'success'; data: any }
| { status: 'error'; message: string }
| { status: 'pending'; progress: number };
function isSuccessResponse(resp: ApiResponse): resp is { status: 'success'; data: any } {
return resp.status === 'success';
}
function handleResponse(response: ApiResponse) {
if (isSuccessResponse(response)) {
console.log('Data:', response.data);
} else if (response.status === 'error') {
console.error('Error:', response.message);
} else {
console.log(`Loading: ${response.progress}%`);
}
}
Practice Tasks
- Write a custom type predicate for checking if a value is a valid email string.
- Create a union of 3 types and handle each with type guards using exhaustiveness checking.
- Implement a function that narrows
unknownto a specific interface using guards.
Key Takeaways
- Use
typeoffor primitives,instanceoffor class instances. - Use
inoperator for interface properties. - Define custom predicates with
iskeyword for reusable guards. - Exhaustiveness checking ensures all union cases are handled.
What's Next?
Next Topic: Explore Async/Await for handling asynchronous operations elegantly.
Decorators: Annotate and Transform Code
Use powerful metadata and transformation patterns with decorators
Note: Decorators are an experimental feature. Enable with "experimentalDecorators": true in tsconfig.json
What Are Decorators?
Decorators are functions that annotate or modify class declarations, methods, accessors, properties, and parameters at design time.
Class Decorators
// Decorator function
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class User {
name: string = 'John';
}
// Class receives decorator as argument
// Can't add properties to User or User.prototype
Method Decorators
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3); // Logs: Calling add [2, 3], Result: 5
Property Decorators
function Validate(target: any, propertyKey: string) {
let value: any;
Object.defineProperty(target, propertyKey, {
get() { return value; },
set(newValue: any) {
if (newValue < 0) {
throw new Error(`${propertyKey} cannot be negative`);
}
value = newValue;
},
});
}
class Product {
@Validate
price: number = 0;
}
const p = new Product();
p.price = 10; // ✅
// p.price = -5; // ❌ Error
Parameter Decorators
function Required(
target: any,
propertyKey: string,
parameterIndex: number
) {
console.log(`Parameter ${parameterIndex} of ${propertyKey} is required`);
}
class User {
setEmail(@Required email: string) {
this.email = email;
}
}
// Metadata is stored; validation logic must be implemented separately
Decorator Factory Pattern
function Retry(times: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
for (let i = 0; i < times; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
if (i === times - 1) throw error;
console.log(`Retry ${i + 1}/${times}`);
}
}
};
return descriptor;
};
}
class ApiClient {
@Retry(3)
async fetchData() {
return fetch('/api/data');
}
}
Real-World Example: ORM Pattern
function Entity(tableName: string) {
return (constructor: Function) => {
Reflect.defineMetadata('table', tableName, constructor);
};
}
function Column(options?: { type?: string; nullable?: boolean }) {
return (target: any, propertyKey: string) => {
Reflect.defineMetadata('column', options, target, propertyKey);
};
}
@Entity('users')
class User {
@Column({ type: 'int' })
id: number;
@Column({ type: 'string' })
name: string;
}
// Metadata can be used for ORM operations
Practice Tasks
- Create a logging decorator that tracks method execution time.
- Write a caching decorator that memoizes function results.
- Build a validation decorator for class properties.
- Implement a throttle decorator that limits method call frequency.
Key Takeaways
- Enable decorators in tsconfig.json with
experimentalDecorators. - Class, method, property, and parameter decorators have different signatures.
- Decorator factories allow parameterized configuration.
- Use with care; they add complexity but enable powerful patterns.
What's Next?
Next Topic: Learn Modules for organizing and reusing code across files.
Modules: Code Organization and Reuse
Master ES6 modules for scalable, maintainable codebases
Why Modules?
Modules encapsulate code, manage dependencies, and prevent global namespace pollution. They're essential for large-scale applications.
Named Exports
// math.ts
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export const subtract = (a: number, b: number): number => a - b;
// app.ts
import { add, subtract, PI } from './math';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
// Import everything
import * as Math from './math';
console.log(Math.add(1, 2));
Default Export
// logger.ts
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
export default Logger;
// ===
// app.ts
import Logger from './logger';
const log = new Logger();
log.log('Hello'); // [LOG] Hello
Mixing Default and Named Exports
// database.ts
class Database {
connect() { /* ... */ }
}
export default Database;
export const createConnection = () => new Database();
export const tables = ['users', 'posts'];
// app.ts
import Database, { createConnection, tables } from './database';
const db = new Database();
const conn = createConnection();
console.log(tables); // ['users', 'posts']
Re-exports
// utils/index.ts
export { add, subtract } from './math';
export { default as Logger } from './logger';
export * as StringUtils from './strings';
// ===
// app.ts
import { add, Logger, StringUtils } from './utils';
// All imports from single file!
Renaming Imports and Exports
// Export with different name
export { User as AuthUser, User as DbUser };
// Import with different name
import { add as addition, subtract as subtraction } from './math';
const result = addition(5, 3);
// Rename default
import Logger as DebugLogger from './logger';
Organizing Code: Module Structure
// src/
// models/
// User.ts
// Post.ts
// index.ts (barrel)
// services/
// UserService.ts
// PostService.ts
// index.ts (barrel)
// utils/
// formatters.ts
// validators.ts
// index.ts (barrel)
// index.ts (root barrel)
// src/models/index.ts
export { default as User } from './User';
export { default as Post } from './Post';
// app.ts
import { User, Post } from './models'; // Clean!
Module Resolution
Common strategies: "node" (CommonJS style), "bundler" (modern), "classic". Configure in tsconfig.json under compilerOptions.moduleResolution
Practice Tasks
- Create a module with both default and named exports; import both types.
- Organize related functions/classes into a barrel index.ts.
- Write a utility module with pure functions and re-export from a main file.
- Practice aliasing imports to avoid naming conflicts.
Key Takeaways
- Use named exports for multiple exports; default export for primary entity.
- Barrel files (index.ts) simplify imports and enable clean APIs.
- Re-exports aggregate modules; great for organizing large codebases.
- Alias imports to prevent conflicts and improve readability.
What's Next?
Next Topic: Learn about Utility Types to transform and manipulate existing types.
Namespaces: Module Organization (Legacy)
Understand namespaces, their uses, and modern alternatives
What Are Namespaces?
Namespaces are TypeScript's way to organize code into logical groups. They're largely replaced by ES6 modules but remain useful in specific scenarios.
Basic Namespace
namespace Math {
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export const PI = 3.14159;
}
// Usage
Math.add(2, 3); // 5
console.log(Math.PI); // 3.14159
Nested Namespaces
namespace App {
export namespace Utils {
export function formatDate(date: Date): string {
return date.toISOString();
}
}
export namespace Services {
export class UserService {
getUser(id: number) { /* ... */ }
}
}
}
// Usage
App.Utils.formatDate(new Date());
const userService = new App.Services.UserService();
Namespace Merging
// validation.ts
namespace Validation {
export function isEmail(email: string): boolean {
return email.includes('@');
}
}
// utils.ts
namespace Validation {
export function isPhone(phone: string): boolean {
return /\\d{10}/.test(phone);
}
}
// Both functions in one namespace
console.log(Validation.isEmail('test@example.com'));
console.log(Validation.isPhone('1234567890'));
Using Namespaces with Modules
// math.ts
export namespace Math {
export function add(a: number, b: number): number {
return a + b;
}
}
// app.ts
import { Math as MathNamespace } from './math';
MathNamespace.add(2, 3);
Namespaces vs ES6 Modules
Best Practice: Use ES6 modules (import/export) for new projects. Namespaces are primarily for legacy code or browser-based scripts.
| Feature | Namespaces | ES6 Modules |
|---|---|---|
| Scope | Global namespace pollution | File scope (isolated) |
| Usage | Browser globals, legacy | Modern standard |
| Tooling | Limited support | Excellent support |
| Dependencies | Manual file ordering | Automatic resolution |
Practice Tasks
- Create a simple namespace with multiple functions.
- Merge two namespace declarations.
- Compare namespace usage with ES6 modules.
- Understand when namespaces are still useful (browser scripts, libraries).
Key Takeaways
- Namespaces organize code into logical groups at runtime.
- ES6 modules are the modern standard and preferred approach.
- Namespaces can merge declarations across files.
- Use namespaces for legacy code compatibility, not new projects.
What's Next?
Congratulations! You've mastered TypeScript. All sections complete. Continue with Bootstrap, jQuery, or Tailwind CSS learning paths.
Async and Await: Modern Async Programming
Write asynchronous code that reads like synchronous code
Why Async/Await?
Async/await simplifies Promise handling by letting you write async code in a synchronous style, making it easier to read, write, and debug.
Async Functions
// Function always returns a Promise
async function fetchUser(id: number) {
return { id, name: 'Alice', email: 'alice@example.com' };
}
// These are equivalent
fetchUser(1); // Promise<{id: number; name: string; email: string}>
// Usage
fetchUser(1).then(user => console.log(user));
Await Keyword
async function getUserData(id: number) {
// Pause until promise resolves
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Can only use await inside async function
// const user = await fetchUser(1); // ❌ Error
// Top-level await (modern)
await getUserData(1);
Error Handling
async function safeGetUser(id: number) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
Sequential vs Parallel Operations
async function slowProcess() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
// Takes: sum of all times
return { user, posts, comments };
}
async function fastProcess() {
const user = await fetchUser(1);
// Start both in parallel
const [posts, friends] = await Promise.all([
fetchPosts(user.id),
fetchFriends(user.id),
]);
// Takes: max of the two times
return { user, posts, friends };
}
Typing Async Functions
interface User {
id: number;
name: string;
}
// Explicit return type
async function getUser(id: number): Promise {
return { id, name: 'Alice' };
}
// Function type with async
type FetchFn = (id: number) => Promise;
const fetchUser: FetchFn = async (id: number) => {
return { id, name: 'Bob' };
};
Real-World Example: Data Pipeline
async function loadDashboard(userId: number) {
try {
// 1. Fetch user
const user = await fetchUser(userId);
if (!user) throw new Error('User not found');
// 2. Fetch related data in parallel
const [projects, tasks, reports] = await Promise.all([
fetchProjects(userId),
fetchTasks(userId),
fetchReports(userId),
]);
return { user, projects, tasks, reports };
} catch (error) {
console.error('Dashboard load failed:', error);
throw error;
}
}
Practice Tasks
- Convert a Promise chain (using .then()) to async/await.
- Write a function that retries a failed async operation up to 3 times.
- Implement Promise.all() to fetch multiple resources in parallel.
- Create a timeout wrapper that rejects after a specified duration.
Key Takeaways
asyncfunctions always returnPromises.awaitpauses execution until promise resolves; use only in async functions.- Use try-catch for error handling in async contexts.
- Use
Promise.all()for parallel operations; avoid unnecessary sequential awaits.
What's Next?
Next Topic: Master Decorators for annotating and modifying classes and methods.
Advanced Types: Master Complex Type Patterns
Unlock powerful patterns for sophisticated type systems
Template Literal Types
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'large';
// Combine literals
type ColoredSize = `${Color}-${Size}`;
// 'red-small' | 'red-large' | 'green-small' | ...
// Event names
type EventName = `on${Capitalize<'click' | 'submit'>}`;
// 'onClick' | 'onSubmit'
// Key patterns
type Getter = `get${Capitalize}`;
type GetterName = Getter<'name'>; // 'getName'
Recursive Types
// Deeply flatten nested arrays
type DeepFlat =
T extends Array
? DeepFlat
: T;
type X = DeepFlat<[[[string]]]>; // string
// Tree structure
type TreeNode = {
value: T;
left?: TreeNode;
right?: TreeNode;
};
const tree: TreeNode = {
value: 1,
left: { value: 2 },
right: { value: 3, left: { value: 4 } },
};
Branded Types
// Create distinct types from primitives
type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };
function createUserId(id: string): UserId {
return id as UserId;
}
function sendEmail(to: Email, message: string) {
console.log(`Sending to ${to}: ${message}`);
}
const id = createUserId('user-123');
const email = 'alice@example.com' as Email;
sendEmail(email, 'Hello'); // ✅
// sendEmail(id, 'Hello'); // ❌ Type error
Type-Level Programming: Linked Lists
type Node = [T, ...Next];
// Build a list
type List1 = Node<1, Node<2, Node<3, []>>>;
// [1, 2, 3]
// Type-level length
type Length = T['length'];
type Len = Length; // 3
// Reverse
type Reverse<
T extends any[],
Acc extends any[] = []
> = T extends [infer Head, ...infer Tail]
? Reverse
: Acc;
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]
Function Overload Signatures
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 r1 = process('hello'); // string
const r2 = process(5); // number
// Generic overloads
function merge(obj1: T, obj2: U): T & U;
function merge(obj1: T, obj2: Partial): T;
function merge(obj1: any, obj2: any) {
return { ...obj1, ...obj2 };
}
Real-World Pattern: Builder Pattern
class QueryBuilder {
private fields: (keyof T)[] = [];
select(field: K): QueryBuilder {
this.fields.push(field as any);
return this as any;
}
build(): T {
return {} as T;
}
}
const query = new QueryBuilder()
.select('name')
.select('email')
.select('age')
.build(); // { name?: any; email?: any; age?: any }
Practice Tasks
- Create branded types for database IDs to prevent mixing different ID types.
- Build a recursive tree type and instantiate it with sample data.
- Write template literal types to generate CSS class name types.
- Implement function overloads for flexible APIs.
Key Takeaways
- Template literal types manipulate strings at the type level.
- Recursive types enable self-referential definitions for trees and graphs.
- Branded types create distinct types from primitives for safety.
- Function overloads provide multiple type-safe signatures.
What's Next?
Next Topic: Explore Compiler Options and tsconfig.json configuration.
Utility Types: Transform Your Types
Master TypeScript's built-in type transformations for maximum code reuse
What Are Utility Types?
Utility types are generic type constructors that transform existing types. They enable DRY principles by letting you build new types from existing ones.
Partial <T>
interface User {
id: number;
name: string;
email: string;
}
type UpdateUser = Partial; // All properties optional
const update: UpdateUser = { name: 'Bob' }; // ✅
// Useful for update operations
function updateUser(id: number, changes: Partial) {
// Only update provided properties
}
Required <T>
interface User {
id: number;
name?: string;
email?: string;
}
type CompleteUser = Required; // All properties required
const user: CompleteUser = {
id: 1,
name: 'Alice',
email: 'alice@example.com', // ✅
};
Pick <T, K>
interface User {
id: number;
name: string;
email: string;
password: string;
}
type UserPreview = Pick;
const preview: UserPreview = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
// password omitted
};
Omit <T, K>
interface User {
id: number;
name: string;
email: string;
password: string;
}
type UserPublic = Omit;
const publicUser: UserPublic = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
// password excluded
};
Record <K, T>
type Status = 'pending' | 'completed' | 'failed';
type StatusCounts = Record;
const counts: StatusCounts = {
pending: 5,
completed: 10,
failed: 2,
};
// Useful for lookup maps
type UserRoles = Record<'admin' | 'user' | 'guest', string[]>;
const permissions: UserRoles = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read'],
};
Exclude <T, U>
type Status = 'pending' | 'completed' | 'failed' | 'cancelled';
type TerminalStatus = Exclude; // 'completed' | 'failed' | 'cancelled'
function markDone(status: TerminalStatus) {
// Only accepts terminal statuses
}
Extract <T, U>
type Status = 'pending' | 'completed' | 'failed';
type SuccessStatus = Extract; // 'completed'
type StringOrNumber = Extract; // string | number
Readonly <T>
interface User {
id: number;
name: string;
}
type ImmutableUser = Readonly;
const user: ImmutableUser = { id: 1, name: 'Alice' };
// user.name = 'Bob'; // ❌ Error
Real-World Example: API Response
interface UserData {
id: number;
email: string;
password: string;
createdAt: Date;
role: string;
}
// API response (public)
type UserResponse = Pick;
// Update form (allow partial updates)
type UserUpdate = Partial>;
// Internal storage (everything)
type UserRecord = Required;
Practice Tasks
- Create a User type and derive PublicUser using Pick or Omit.
- Build a Settings type with all properties optional using Partial.
- Use Record to create a lookup table for permissions by role.
- Combine utility types: Readonly
> for immutable partial objects.
Key Takeaways
Partialmakes properties optional;Requiredmakes them mandatory.Pickincludes specific keys;Omitexcludes them.Recordcreates objects with specific keys and value types.- Combine utility types for complex transformations.
What's Next?
Next Topic: Explore Mapped Types for creating types that transform existing types dynamically.
Mapped Types: Transform Types by Iterating Properties
Create flexible types by dynamically transforming existing ones
What Are Mapped Types?
Mapped types iterate over properties of existing types and create new types by transforming each property. They're powerful for avoiding repetition.
Basic Mapped Type
// Make all properties getters
type Getters = {
[K in keyof T]: () => T[K];
};
interface User {
name: string;
email: string;
}
type UserGetters = Getters;
// Results in:
// {
// name: () => string;
// email: () => string;
// }
Common Mapped Type Patterns
// Make all properties optional
type Optional = {
[K in keyof T]?: T[K];
};
// Make all properties readonly
type ReadonlyVersion = {
readonly [K in keyof T]: T[K];
};
// Wrap each value in Promise
type Promises = {
[K in keyof T]: Promise;
};
type UserPromises = Promises;
// {
// name: Promise;
// email: Promise;
// }
Filtering Properties with as
// Add prefix to keys
type WithGetPrefix = {
[K in keyof T as `get${Capitalize}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = WithGetPrefix;
// Results in:
// {
// getName: () => string;
// getAge: () => number;
// }
// Filter only string properties
type StringProperties = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringProperties; // { name: string }
Conditional Mapping
type Flatten = {
[K in keyof T]: T[K] extends Array ? U : T[K];
};
interface Data {
items: string[];
count: number;
tags: string[];
}
type FlatData = Flatten;
// Results in:
// {
// items: string;
// count: number;
// tags: string;
// }
Real-World Example: Event Handlers
interface Events {
click: { x: number; y: number };
submit: { formData: any };
error: { message: string };
}
type EventHandlers = {
[K in keyof T]: (event: T[K]) => void;
};
type AllHandlers = EventHandlers;
// {
// click: (event: { x: number; y: number }) => void;
// submit: (event: { formData: any }) => void;
// error: (event: { message: string }) => void;
// }
const handlers: AllHandlers = {
click: (e) => console.log(e.x, e.y),
submit: (e) => console.log(e.formData),
error: (e) => console.error(e.message),
};
Advanced Pattern: Getters and Setters
interface User {
id: number;
name: string;
email: string;
}
type Getters = {
[K in keyof T as `get${Capitalize}`]: () => T[K];
};
type Setters = {
[K in keyof T as `set${Capitalize}`]: (value: T[K]) => void;
};
type API = Getters & Setters;
type UserAPI = API;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// setId: (value: number) => void;
// setName: (value: string) => void;
// setEmail: (value: string) => void;
// }
Practice Tasks
- Create a mapped type that makes all properties optional.
- Build a mapped type that prefixes all property names with "is".
- Implement a type that wraps each property value in a Promise.
- Write a mapped type that filters and keeps only numeric properties.
Key Takeaways
- Use
keyof Tto iterate over type properties. - Use
asclause for key remapping and filtering. - Combine with conditional types for sophisticated transformations.
- Mapped types eliminate duplication in type definitions.
What's Next?
Next Topic: Master Conditional Types for logic-based type selection.
Conditional Types: Logic-Based Type Selection
Use type-level conditionals to create intelligent, context-aware types
What Are Conditional Types?
Conditional types enable type-level logic using a ternary syntax: T extends U ? X : Y. They select types based on type relationships.
Basic Conditional Type
type IsString = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString; // false
// Practical example: Extract return type
type Flatten = T extends Array ? U : T;
type Str = Flatten; // string
type Num = Flatten; // number
Using infer for Type Extraction
// Extract return type from function
type ReturnType = T extends (...args: any[]) => infer R ? R : never;
type FuncType = (x: number) => string;
type Result = ReturnType; // string
// Extract Promise inner type
type Awaited = T extends Promise ? U : T;
type P = Awaited>; // string
type N = Awaited; // number
Distributive Conditional Types
type IsArray = T extends Array ? true : false;
// Single type
type A = IsArray; // true
// Union: applies to EACH member
type B = IsArray; // true | false
// Filter union types
type Extract = T extends U ? T : never;
type StringOrNumber = Extract;
// Results in: string | number
Chaining Conditions
type Flatten =
T extends Array
? Flatten // Recursively flatten
: T;
type Deep = Flatten<[[[string]]]>; // string
// Type narrowing chain
type TypeName =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object';
type T1 = TypeName<42>; // 'number'
type T2 = TypeName<{ x: 1 }>; // 'object'
Real-World Example: Flexible API
type ApiCall =
U extends void
? Promise
: Promise;
// Single result
type SingleUser = ApiCall<{ id: number; name: string }>;
// Promise<{ id: number; name: string }>
// Multiple results (U provided)
type MultipleUsers = ApiCall<{ id: number; name: string }, 'multiple'>;
// Promise>
Avoiding Distribution
// Distributive (applies to each union member)
type IsArray1 = T extends Array ? true : false;
type Result1 = IsArray1; // boolean | true
// Non-distributive (tuple prevents distribution)
type IsArray2 = [T] extends [Array] ? true : false;
type Result2 = IsArray2; // false
Practice Tasks
- Write a conditional type that returns "primitive" or "object" based on input.
- Create a type that extracts the inner type from a Promise, Array, or returns the type unchanged.
- Implement distributive logic to filter a union to only string types.
- Build a recursive flatten type that deeply unwraps nested arrays.
Key Takeaways
- Conditional types use
extends ? :syntax for type-level logic. inferkeyword extracts types from complex structures.- Distributive conditionals automatically apply to each union member.
- Wrap in tuples to prevent distribution when needed.
What's Next?
Next Topic: Understand Type Inference to let TypeScript determine types automatically.
Type Inference: Let TypeScript Figure It Out
Reduce verbosity by letting the compiler infer types from context
What Is Type Inference?
Type inference automatically determines types based on assigned values, function returns, and context. It reduces manual type annotations without sacrificing safety.
Variable Inference
// Type inferred from value
const name = 'Alice'; // string
const age = 30; // number
const active = true; // boolean
const items = ['a', 'b']; // string[]
const coords = { x: 0, y: 0 }; // { x: number; y: number }
// Explicit type overrides inference
const count: number = 5;
// const assertion (literal types)
const mode = 'dev' as const; // 'dev' (not string)
Function Return Type Inference
// Return type inferred
function add(a: number, b: number) {
return a + b; // number
}
function greet(name: string) {
if (name === 'Admin') {
return { role: 'admin' }; // { role: string }
}
return { role: 'user' }; // { role: string }
}
// Complex return with multiple paths
function process(id: string | number) {
return typeof id === 'string' ? id.toUpperCase() : id.toString();
// string
}
Contextual Typing
// Callback parameters inferred from context
const items = ['a', 'b', 'c'];
items.forEach(item => {
// item is automatically inferred as string
console.log(item.toUpperCase());
});
const squared = items.map((item, index) => {
// item: string, index: number
return item.length + index;
});
type Callback = (error: Error | null, data: string) => void;
const handler: Callback = (err, data) => {
// err: Error | null, data: string (inferred)
};
Generic Type Inference
function identity(value: T): T {
return value;
}
// T inferred from argument
const result1 = identity('hello'); // T = string
const result2 = identity(42); // T = number
function createArray(item: T): T[] {
return [item];
}
const arr = createArray([1, 2, 3]); // T = number[]
const Assertion
// Without as const: inferred as string
const direction1 = 'up'; // string
// With as const: inferred as literal 'up'
const direction2 = 'up' as const; // 'up'
const colors = ['red', 'green', 'blue'] as const;
// readonly ['red', 'green', 'blue']
const status = { code: 200, message: 'OK' } as const;
// {
// readonly code: 200;
// readonly message: 'OK';
// }
When to Annotate Explicitly
// 1. Ambiguous structure
const person = { name: 'Alice' }; // Could be more specific
const employee: { name: string; role: string } = { name: 'Alice', role: 'dev' };
// 2. Public API contracts
export function process(data: string[]): boolean {
return data.length > 0;
}
// 3. Explicit intent
const config: { [key: string]: any } = { debug: true, port: 3000 };
// 4. Breaking type widening
let response = { status: 200 }; // { status: number }
let response2: { status: 200 } = { status: 200 }; // { status: 200 }
Practice Tasks
- Write functions and let inference determine return types; verify with IDE.
- Use contextual typing with callbacks in array methods (map, filter).
- Apply
as constto create literal types for configuration objects. - Experiment with generic functions where type parameters are inferred.
Key Takeaways
- TypeScript infers types from values, returns, and context automatically.
- Annotate when intent is ambiguous or for public API contracts.
- Use
as constfor literal types and configuration objects. - Let generic types be inferred from arguments when possible.
What's Next?
Next Topic: Explore Compiler Options to configure TypeScript behavior.
Compiler Options: Tune TypeScript Behavior
Control how TypeScript compiles and validates your code
Essential Compiler Options
Compiler options in tsconfig.json control type checking strictness, output format, module resolution, and more.
Strict Mode Options
{
"compilerOptions": {
"strict": true, // Enable all strict options
"noImplicitAny": true, // Error on implicit any
"strictNullChecks": true, // Treat null/undefined strictly
"strictFunctionTypes": true, // Strict function parameter types
"strictBindCallApply": true, // Strict bind/call/apply
"strictPropertyInitialization": true, // Properties must be initialized
"noImplicitThis": true, // Error on implicit this
"alwaysStrict": true // Use strict mode in output
}
}
Module and Target Options
{
"compilerOptions": {
// Output target
"target": "ES2020", // ES2015, ES2020, ES2021, ESNext
// Module system
"module": "ESNext", // commonjs, esnext, umd, amd, es2015
// Module resolution
"moduleResolution": "node", // classic, node
// Path mapping
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"]
}
}
}
Output Options
{
"compilerOptions": {
// Output directory
"outDir": "./dist",
"rootDir": "./src",
// Source maps for debugging
"sourceMap": true,
"inlineSourceMap": true,
// Declaration files
"declaration": true,
"declarationMap": true,
// Remove comments, whitespace
"removeComments": true,
// Import helpers
"importHelpers": true,
"lib": ["ES2020", "DOM"]
}
}
Type Checking Options
{
"compilerOptions": {
// Strict checks
"noUnusedLocals": true, // Warn about unused variables
"noUnusedParameters": true, // Warn about unused parameters
"noImplicitReturns": true, // Ensure all code paths return
"noFallthroughCasesInSwitch": true, // Error on case fall-through
// Looser checks (if needed)
"allowJs": true, // Allow JavaScript files
"checkJs": true, // Type-check JavaScript files
"skipLibCheck": true // Skip type checking of libraries
}
}
Practical Configuration Example
{
"compilerOptions": {
// Strict
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
// Output
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Libraries
"lib": ["ES2020", "DOM"],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Practice Tasks
- Start with
"strict": trueand observe which code fails type checks. - Configure path aliases in tsconfig and use them in imports.
- Enable
noUnusedLocalsand refactor code to remove unused variables. - Generate declaration files and explore the .d.ts output.
Key Takeaways
- Enable
strictmode for maximum type safety. - Configure
targetandmodulefor your runtime environment. - Use
noUnusedLocals/ParametersandnoImplicitReturnsfor quality code. - Path aliases simplify imports and enable easy refactoring.
What's Next?
Next Topic: Dive into TypeScript Configuration (tsconfig.json) to master all settings.
TypeScript Configuration (tsconfig.json): Master Your Setup
Organize complex TypeScript projects with strategic configuration
tsconfig.json Structure
{
"compilerOptions": {
// Strictness
"strict": true,
// Output
"outDir": "./dist",
"rootDir": "./src",
// Modules
"module": "ESNext",
"target": "ES2020"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"compileOnSave": false,
"ts-node": { "transpileOnly": true }
}
File Include/Exclude
{
"include": [
"src/**/*", // All files under src
"tests/**/*.ts" // Test files
],
"exclude": [
"node_modules", // Never include dependencies
"dist",
"**/*.spec.ts", // Exclude test files if not wanted
"**/node_modules/**"
]
}
Configuration Inheritance with extends
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"target": "ES2020",
"module": "ESNext"
}
}
// tsconfig.json (extends base)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src/**/*"]
}
// tsconfig.test.json (extends base, different settings)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs", // Override for tests
"sourceMap": true
},
"include": ["tests/**/*"]
}
Development vs Production Config
// tsconfig.dev.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": true, // Include source maps
"skipLibCheck": false, // Full checking
"declaration": false // Don't generate .d.ts
}
}
// tsconfig.prod.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"skipLibCheck": true, // Speed up builds
"declaration": true, // Generate .d.ts for library
"removeComments": true // Smaller output
}
}
Monorepo Configuration
// tsconfig.json (root)
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true
},
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/cli" }
]
}
// packages/core/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"references": []
}
Path Aliases (baseUrl)
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"],
"@api/*": ["src/api/*"]
}
}
}
// In code:
// Instead of: import { Button } from '../../../components/Button'
// Use: import { Button } from '@components/Button'
Practice Tasks
- Create a tsconfig.base.json and have tsconfig.json extend it.
- Set up separate configs for development, production, and testing.
- Configure path aliases and refactor imports to use them.
- Create a monorepo structure with project references.
Key Takeaways
- Use
extendsto share configuration across multiple tsconfig files. - Separate configs for dev, test, and prod optimize for each environment.
- Project references enable efficient monorepo builds.
- Path aliases make imports cleaner and refactoring easier.
What's Next?
Next Topic: Learn Declaration Files to provide types for JavaScript libraries.
Declaration Files (.d.ts): Add Types to JavaScript
Provide type information for JavaScript libraries and legacy code
What Are Declaration Files?
Declaration files (.d.ts) describe the shape and types of JavaScript code. They enable TypeScript type checking without TypeScript source files.
Creating Declaration Files
// math.d.ts
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export const PI: number;
// Corresponding JavaScript (math.js)
// export function add(a, b) { return a + b; }
// export function subtract(a, b) { return a - b; }
// export const PI = 3.14159;
Declaring Classes
// database.d.ts
export class Database {
constructor(connectionString: string);
connect(): Promise;
disconnect(): Promise;
query(sql: string, params?: any[]): Promise;
close(): void;
}
export interface QueryResult {
rowCount: number;
rows: any[];
}
Ambient Declarations
// globals.d.ts
declare global {
interface Window {
appConfig: { apiUrl: string; debug: boolean };
gtag: (command: string, ...args: any[]) => void;
}
namespace NodeJS {
interface ProcessEnv {
API_KEY: string;
DATABASE_URL: string;
}
}
}
// Now available everywhere
const config = window.appConfig;
const apiKey = process.env.API_KEY;
Generic Type Declarations
// api.d.ts
export interface ApiResponse {
success: boolean;
data?: T;
error?: string;
}
export function fetchData(
endpoint: string,
options?: RequestInit
): Promise>;
export class ApiClient {
baseUrl: string;
get(path: string): Promise;
post(path: string, body: any): Promise;
}
Namespace and Module Augmentation
// express-extensions.d.ts
// Augment Express Request type
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
correlationId?: string;
}
}
}
// lodash-extensions.d.ts
// Add custom lodash method
import * as _ from 'lodash';
declare module 'lodash' {
function customSort(arr: any[], key: string): any[];
}
Auto-Generating Declaration Files
// tsconfig.json
{
"compilerOptions": {
"declaration": true, // Generate .d.ts files
"declarationMap": true, // Include source maps
"declarationDir": "./types", // Output directory
"emitDeclarationOnly": false // Also emit JS
}
}
// package.json
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
"typings": "dist/index.d.ts" // Alternative
}
Publishing Types to DefinitelyTyped
// DefinitelyTyped/types/my-library/index.d.ts
export interface Options {
timeout?: number;
debug?: boolean;
}
export function createInstance(options?: Options): Instance;
export class Instance {
start(): void;
stop(): void;
on(event: string, callback: Function): void;
}
// Users install via npm
// npm install @types/my-library
Practice Tasks
- Write a .d.ts file for a simple JavaScript module.
- Create ambient declarations for global variables in your project.
- Enable
declaration: truein tsconfig and auto-generate types from TypeScript source. - Use module augmentation to extend existing library types.
Key Takeaways
- Declaration files provide type information for JavaScript code.
- Ambient declarations add types to globals and external libraries.
- TypeScript can auto-generate .d.ts files; set
declaration: true. - Module augmentation extends existing type definitions safely.
What's Next?
Next Topic: Learn Testing TypeScript with Jest and best practices.
Testing TypeScript: Jest and Best Practices
Write reliable tests for TypeScript code with confidence
Setting Up Jest with TypeScript
npm install --save-dev jest @types/jest ts-jest typescript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
moduleFileExtensions: ['ts', 'js'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
],
};
Basic Testing Pattern
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// math.test.ts
import { add, multiply } from './math';
describe('Math functions', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('should multiply two numbers', () => {
expect(multiply(2, 3)).toBe(6);
});
});
Testing Classes and Types
// User.ts
export class User {
constructor(private name: string, private email: string) {}
getName(): string {
return this.name;
}
isValidEmail(): boolean {
return this.email.includes('@');
}
}
// User.test.ts
import { User } from './User';
describe('User class', () => {
let user: User;
beforeEach(() => {
user = new User('Alice', 'alice@example.com');
});
it('should return the name', () => {
expect(user.getName()).toBe('Alice');
});
it('should validate email', () => {
expect(user.isValidEmail()).toBe(true);
});
});
Mocking and Spying
// api.ts
export async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// api.test.ts
import * as api from './api';
describe('API functions', () => {
it('should fetch user data', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: 1, name: 'Alice' }),
})
) as jest.Mock;
const user = await api.fetchUser(1);
expect(user.name).toBe('Alice');
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
});
Testing Async Code
// service.ts
export async function processData(data: string[]): Promise {
return new Promise((resolve) => {
setTimeout(() => resolve(data.length), 100);
});
}
// service.test.ts
import { processData } from './service';
describe('Async service', () => {
it('should process data asynchronously', async () => {
const result = await processData(['a', 'b', 'c']);
expect(result).toBe(3);
});
// Alternative: return promise
it('should work with return', () => {
return processData(['x', 'y']).then((result) => {
expect(result).toBe(2);
});
});
});
Testing with Type Safety
// testUtils.ts
export function expectType(value: T, expected: T): void {
expect(value).toEqual(expected);
}
// Usage
type User = { id: number; name: string };
const user: User = { id: 1, name: 'Alice' };
expectType(user, { id: 1, name: 'Alice' });
// Mock with types
export function createMock(partial: Partial): T {
return partial as T;
}
const mockUser = createMock({ name: 'Bob' });
Test Coverage
npm test -- --coverage
# Output
# --------|----------|----------|----------|----------|---|
# File | % Stmts | % Branch | % Funcs | % Lines |
# --------|----------|----------|----------|----------|---|
# math.ts | 100 | 100 | 100 | 100 |
Practice Tasks
- Set up Jest with ts-jest in a new project.
- Write unit tests for a class with multiple methods.
- Mock an external API call using jest.fn().
- Test async functions with async/await.
- Run tests with coverage and aim for >80% coverage.
Key Takeaways
- Use ts-jest preset for seamless TypeScript testing in Jest.
- Mock external dependencies to isolate code under test.
- Use async/await for testing async code.
- Aim for high coverage but prioritize meaningful tests.
What's Next?
Next Topic: Learn Testing and Linting together for code quality.
Migrating to TypeScript: Gradual Adoption
Convert existing JavaScript projects to TypeScript incrementally
Why Migrate?
TypeScript provides early error detection, better tooling, self-documenting code, and easier refactoring—all while maintaining JavaScript flexibility.
Phase 1: Setup and Configuration
{
"compilerOptions": {
"target": "ES2020",
"module": "esnext",
"allowJs": true,
"checkJs": false,
"strict": false
}
}
Phase 2: Convert Files Incrementally
// Step 1: Setup tsconfig with allowJs
// Step 2: Add @types packages for dependencies
// npm install --save-dev @types/node
// Step 3: Convert utilities first (leaf modules)
// utils/formatters.ts
export function formatDate(date: Date): string {
return date.toISOString();
}
Key Takeaways
- Start with
allowJs: trueandstrict: false. - Convert modules from leaf to root (bottom-up approach).
- Incrementally enable stricter compiler options over time.
What's Next?
Next Topic: Master TypeScript with React for modern web development.
TypeScript Best Practices: Writing Professional Code
Master the patterns and practices that lead to maintainable, type-safe TypeScript applications
Essential TypeScript Best Practices
Following TypeScript best practices ensures your code is type-safe, maintainable, and leverages the full power of the type system. These guidelines are drawn from years of community experience and official recommendations.
1. Always Enable Strict Mode
{
"compilerOptions": {
"strict": true, // Enable all strict checks
// Or enable individually:
"strictNullChecks": true, // Prevent null/undefined errors
"strictFunctionTypes": true, // Strict function parameter checking
"strictBindCallApply": true, // Strict bind/call/apply
"strictPropertyInitialization": true, // Check class properties initialized
"noImplicitThis": true, // Require explicit 'this' types
"noImplicitAny": true, // Ban implicit any types
"alwaysStrict": true // Parse in strict mode
}
}
2. Avoid the any Type
// ❌ BAD: Defeats purpose of TypeScript
function process(data: any) {
return data.value; // No type safety
}
// ✅ GOOD: Use unknown when type is truly unknown
function processSafe(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: any }).value;
}
}
// ✅ GOOD: Use generics for flexibility with safety
function processGeneric(data: T): T {
return data;
}
// ✅ GOOD: Use union types for known possibilities
function processUnion(data: string | number | boolean) {
// Handle each type
}
// ✅ GOOD: Define proper interfaces
interface Data {
value: string;
count: number;
}
function processTyped(data: Data) {
return data.value; // Full type safety
}
3. Leverage Type Inference
// ❌ BAD: Redundant type annotations
const name: string = "Alice";
const age: number = 25;
const items: number[] = [1, 2, 3];
// ✅ GOOD: Let TypeScript infer obvious types
const name = "Alice"; // inferred as string
const age = 25; // inferred as number
const items = [1, 2, 3]; // inferred as number[]
// ✅ GOOD: Annotate when helpful
let userId: string | number; // Can't infer without value
userId = "abc123";
userId = 42;
// ✅ GOOD: Always annotate function parameters
function greet(name: string, age: number) {
return `Hello ${name}, you are ${age}`;
}
// ✅ GOOD: Annotate return types for clarity
function calculateTotal(items: number[]): number {
return items.reduce((sum, item) => sum + item, 0);
}
4. Use Interfaces for Object Shapes
// ✅ GOOD: Use interfaces for objects
interface User {
id: number;
name: string;
email: string;
}
// ✅ GOOD: Interfaces can be extended
interface AdminUser extends User {
permissions: string[];
}
// ✅ GOOD: Use type aliases for unions/primitives
type Status = "pending" | "approved" | "rejected";
type ID = string | number;
// ✅ GOOD: Type aliases for complex types
type AsyncResult = Promise<{ data: T; error: null } | { data: null; error: Error }>;
// ❌ AVOID: Type alias for simple objects (use interface)
type UserType = {
id: number;
name: string;
};
5. Use Readonly and Const Appropriately
// ✅ GOOD: Readonly for immutable properties
interface Config {
readonly apiUrl: string;
readonly apiKey: string;
timeout?: number;
}
// ✅ GOOD: Readonly arrays
function processItems(items: readonly string[]) {
// items.push("new"); // ❌ Error: readonly
return items.map(item => item.toUpperCase()); // ✅ OK
}
// ✅ GOOD: as const for literal types
const ROUTES = {
HOME: "/",
ABOUT: "/about",
CONTACT: "/contact"
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// ✅ GOOD: Readonly utility type
type ReadonlyUser = Readonly;
// ✅ GOOD: ReadonlyArray
const numbers: ReadonlyArray = [1, 2, 3];
6. Prefer Union Types Over Enums
// ✅ GOOD: Union types (no runtime cost)
type Status = "pending" | "approved" | "rejected";
function updateStatus(status: Status) {
// Full autocomplete and type safety
}
// ✅ GOOD: Const object with as const
const STATUS = {
PENDING: "pending",
APPROVED: "approved",
REJECTED: "rejected"
} as const;
type StatusValue = typeof STATUS[keyof typeof STATUS];
// ⚠️ OK: Enums when you need iteration or reverse mapping
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
// Benefit: Can iterate
Object.values(HttpMethod).forEach(method => {
console.log(method);
});
7. Use Type Guards for Runtime Checking
// ✅ GOOD: User-defined type guards
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function isDog(pet: Dog | Cat): pet is Dog {
return (pet as Dog).bark !== undefined;
}
function makeSound(pet: Dog | Cat) {
if (isDog(pet)) {
pet.bark(); // TypeScript knows it's Dog
} else {
pet.meow(); // TypeScript knows it's Cat
}
}
// ✅ GOOD: Built-in type guards
function processValue(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
// ✅ GOOD: instanceof for classes
class CustomError extends Error {}
function handleError(error: Error | CustomError) {
if (error instanceof CustomError) {
// Handle custom error
}
}
8. Avoid Type Assertions
// ❌ BAD: Type assertion without validation
function getUser(id: string): User {
const response = fetch(`/api/users/${id}`);
return response as User; // Dangerous!
}
// ✅ GOOD: Proper validation
function getUserSafe(id: string): User {
const response = fetch(`/api/users/${id}`);
if (!isUser(response)) {
throw new Error("Invalid user data");
}
return response;
}
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
typeof obj.id === "number" &&
typeof obj.name === "string" &&
typeof obj.email === "string"
);
}
// ✅ GOOD: Use assertions only when absolutely necessary
const input = document.getElementById("email") as HTMLInputElement;
9. Use Utility Types
interface User {
id: number;
name: string;
email: string;
password: string;
}
// ✅ GOOD: Partial for optional properties
type UserUpdate = Partial;
// ✅ GOOD: Pick for specific properties
type UserPublic = Pick;
// ✅ GOOD: Omit to exclude properties
type UserWithoutPassword = Omit;
// ✅ GOOD: Required for making all properties required
type RequiredUser = Required>;
// ✅ GOOD: Readonly for immutability
type ImmutableUser = Readonly;
// ✅ GOOD: Record for dictionaries
type UserCache = Record;
10. Organize Code with Modules
// ✅ GOOD: Named exports for multiple items
// user.types.ts
export interface User {
id: number;
name: string;
}
export interface AdminUser extends User {
permissions: string[];
}
// ✅ GOOD: Default export for primary export
// UserService.ts
export default class UserService {
getUser(id: number): User {
// Implementation
}
}
// ✅ GOOD: Barrel exports for cleaner imports
// index.ts
export * from "./user.types";
export * from "./user.service";
export { default as UserService } from "./UserService";
// Usage
import { User, AdminUser, UserService } from "./users";
Configuration Best Practices
| Setting | Recommended Value | Why |
|---|---|---|
| strict | true | Enables all strict checking options |
| noImplicitAny | true | Catches missing type annotations |
| strictNullChecks | true | Prevents null/undefined errors |
| esModuleInterop | true | Better CommonJS compatibility |
| skipLibCheck | true | Faster compilation |
| forceConsistentCasingInFileNames | true | Prevents case-sensitivity issues |
Practice Tasks
- Task 1: Enable strict mode in tsconfig.json and fix all errors.
- Task 2: Refactor code to eliminate all any types.
- Task 3: Replace type assertions with proper type guards.
- Task 4: Use utility types to create derived types from base interfaces.
- Task 5: Add readonly modifiers to prevent mutation.
- Task 6: Convert enum to union type for better tree-shaking.
- Task 7: Organize a project with proper module structure.
Common Anti-Patterns to Avoid
❌ Things to Avoid
- Using any everywhere: Defeats TypeScript's purpose
- Type assertions without validation: Runtime errors waiting to happen
- Non-null assertions (!) carelessly: Bypasses safety checks
- Disabling strict mode: Misses many type errors
- Over-complicated types: Keep types simple and readable
- Ignoring compiler errors: @ts-ignore is a last resort
- Not leveraging inference: Redundant annotations clutter code
Key Takeaways
- Enable strict mode always: Catches more errors early
- Avoid any, use unknown: Maintain type safety
- Leverage type inference: Let TypeScript do the work
- Prefer interfaces for objects: Better for extension
- Use readonly for immutability: Prevent accidental mutations
- Type guards over assertions: Runtime safety matters
- Utility types are your friends: Built-in helpers for common patterns
- Organize with modules: Clean, maintainable structure
What's Next?
Next Topic: Congratulations on completing the TypeScript tutorial! Review the sections, practice the concepts, and start building type-safe applications. Consider exploring React with TypeScript or Node.js with TypeScript next.
TypeScript with React: Type-Safe Components
Build robust React applications with full type safety
Typing React Components
import React, { FC, ReactNode } from 'react';
interface ButtonProps {
onClick: () => void;
children: ReactNode;
disabled?: boolean;
}
const Button: FC = ({
onClick,
children,
disabled = false,
}) => {
return (
);
};
Typing Hooks
import React, { useState, useCallback } from 'react';
interface User {
id: number;
name: string;
}
const UserForm: React.FC = () => {
const [user, setUser] = useState(null);
const [count, setCount] = useState(0);
const handleClick = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setCount(prev => prev + 1);
}, []);
return ;
};
Key Takeaways
- Use interfaces for component props.
- Explicitly type useState generics.
- Type React events properly.
- Leverage generics for reusable components.
What's Next?
Next Topic: Master TypeScript with Node.js for backend development.
TypeScript with Node.js: Backend Type Safety
Build scalable, maintainable server applications with TypeScript
Project Setup
npm init -y
npm install --save express
npm install --save-dev typescript ts-node @types/node @types/express nodemon
Express with TypeScript
import express, { Request, Response } from 'express';
const app = express();
const port = process.env.PORT || 3000;
app.use(express.json());
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok' });
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Request and Response Typing
import { Router, Request, Response } from 'express';
interface User {
id: number;
name: string;
email: string;
}
const router = Router();
router.post('/users', (req: Request<{}, {}, User>, res: Response) => {
const { name, email } = req.body;
const user: User = { id: 1, name, email };
res.json(user);
});
export default router;
Middleware with Types
import { Request, Response, NextFunction, RequestHandler } from 'express';
interface AuthRequest extends Request {
user?: { id: number; role: string };
}
const authMiddleware: RequestHandler = (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.user = { id: 1, role: 'admin' };
next();
};
Key Takeaways
- Use
ts-nodefor development. - Extend Express Request for custom properties.
- Type route parameters and request bodies.
- Create custom error handling middleware.
What's Next?
Next Topic: Continue learning with Bootstrap sections.
Last updated: February 2026