
Mastering TypeScript: Advanced Patterns and Best Practices
Dive deep into advanced TypeScript patterns, type system features, and best practices for building scalable applications.
Introduction to Advanced TypeScript
TypeScript has become an essential tool for modern JavaScript development. While many developers are comfortable with basic types, mastering advanced TypeScript patterns can significantly improve your code quality and developer experience.
Advanced Type System Features
Generic Constraints
One of the most powerful features in TypeScript is the ability to constrain generic types:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("Hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
logLength({ length: 10, value: 3 }); // ✅ object has length
Conditional Types
Conditional types allow you to create types that depend on a condition:
type ApiResponse<T> = T extends string
? { message: T }
: T extends number
? { code: T }
: { data: T };
type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { code: number }
type ObjectResponse = ApiResponse<User>; // { data: User }
Mapped Types
Create new types by transforming properties of existing types:
type Optional<T> = {
[P in keyof T]?: T[P];
};
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Optional<User>; // All properties optional
type ImmutableUser = ReadOnly<User>; // All properties readonly
Utility Types in Action
TypeScript provides several built-in utility types that can save you time:
Pick and Omit
interface BlogPost {
id: number;
title: string;
content: string;
author: string;
publishedAt: Date;
tags: string[];
}
// Only pick specific properties
type BlogPreview = Pick<BlogPost, "id" | "title" | "author">;
// Omit specific properties
type CreateBlogPost = Omit<BlogPost, "id" | "publishedAt">;
Record Type
The Record
type is perfect for creating index signatures:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoints = Record<HttpMethod, string[]>;
const endpoints: ApiEndpoints = {
GET: ["/users", "/posts"],
POST: ["/users", "/posts"],
PUT: ["/users/:id", "/posts/:id"],
DELETE: ["/users/:id", "/posts/:id"],
};
Design Patterns with TypeScript
Factory Pattern
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
type ShapeType = "circle" | "rectangle";
class ShapeFactory {
static createShape(type: ShapeType, ...args: number[]): Shape {
switch (type) {
case "circle":
return new Circle(args[0]);
case "rectangle":
return new Rectangle(args[0], args[1]);
default:
throw new Error(`Unknown shape type: ${type}`);
}
}
}
Builder Pattern
class QueryBuilder {
private query: string = "";
select(fields: string[]): this {
this.query += `SELECT ${fields.join(", ")} `;
return this;
}
from(table: string): this {
this.query += `FROM ${table} `;
return this;
}
where(condition: string): this {
this.query += `WHERE ${condition} `;
return this;
}
build(): string {
return this.query.trim();
}
}
const query = new QueryBuilder()
.select(["name", "email"])
.from("users")
.where("active = 1")
.build();
Error Handling Best Practices
Result Type Pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return {
success: false,
error: new Error(`HTTP ${response.status}`),
};
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error("Unknown error"),
};
}
}
// Usage
const result = await fetchUser(123);
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error.message);
}
Performance Considerations
Lazy Types
For large applications, consider using lazy type imports:
// Instead of importing the entire module
import { HeavyLibraryType } from "heavy-library";
// Use dynamic imports for types
type LazyType = import("heavy-library").HeavyLibraryType;
function processData(data: LazyType) {
// Implementation
}
Template Literal Types
Create more specific string types:
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue<T extends string> = `${T}${CSSUnit}`;
type Width = CSSValue<string>; // "10px" | "2em" | "100%" etc.
function setWidth(element: HTMLElement, width: Width) {
element.style.width = width;
}
setWidth(element, "100px"); // ✅
setWidth(element, "invalid"); // ❌ Type error
Testing with TypeScript
Type Testing
Use conditional types to test your type definitions:
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
// Test your types
type Test1 = Expect<Equal<Pick<User, "name">, { name: string }>>;
type Test2 = Expect<Equal<Omit<User, "id">, { name: string; email: string }>>;
Migration Strategies
When migrating JavaScript to TypeScript:
- Start with
any
and gradually add specific types - Use
@ts-ignore
sparingly for quick fixes - Enable strict mode incrementally
- Add types to new code first
Gradual Typing Example
// Step 1: Add basic types
function calculateTotal(items: any[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Step 2: Add more specific types
interface Item {
price: number;
name: string;
}
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Step 3: Add validation
function calculateTotal(items: Item[]): number {
if (!Array.isArray(items)) {
throw new Error("Items must be an array");
}
return items.reduce((sum, item) => {
if (typeof item.price !== "number") {
throw new Error("Item price must be a number");
}
return sum + item.price;
}, 0);
}
Conclusion
Mastering advanced TypeScript patterns takes time and practice, but the benefits are enormous:
- Better code quality through type safety
- Improved developer experience with autocomplete and error detection
- Easier refactoring with confidence
- Self-documenting code through type definitions
Start incorporating these patterns gradually into your projects, and you'll see immediate improvements in your development workflow.
Next Steps
To continue improving your TypeScript skills:
- Practice with the TypeScript playground
- Read the TypeScript handbook
- Contribute to open-source TypeScript projects
- Join the TypeScript community
Happy typing! 🚀