¯ cat typescript-best-practices.mdx
TypeScript Best Practices for Large-Scale Applications
Learn advanced TypeScript patterns and techniques to build maintainable, type-safe applications at scale
¯ cat typescript-best-practices.mdx
Learn advanced TypeScript patterns and techniques to build maintainable, type-safe applications at scale
TypeScript has become the de facto standard for building large-scale JavaScript applications. However, leveraging its full potential requires understanding advanced patterns and best practices. Let's explore how to write better TypeScript code.
any Like the Plague// ❌ Bad
function processData(data: any) {
return data.value;
}
// ✅ Good
interface DataStructure {
value: string;
metadata: Record<string, unknown>;
}
function processData(data: DataStructure) {
return data.value;
}
unknown for True Unknownsfunction parseJSON(jsonString: string): unknown {
return JSON.parse(jsonString);
}
// Force type checking before use
const result = parseJSON('{"name": "John"}');
if (typeof result === 'object' && result !== null && 'name' in result) {
console.log(result.name);
}
TypeScript provides powerful utility types:
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<Partial<User>>;
// Exclude properties
type UserWithoutAge = Omit<User, 'age'>;
Create type-safe state machines:
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: Error };
function handleState(state: LoadingState) {
switch (state.status) {
case 'idle':
return 'Ready to start';
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${state.data}`; // TypeScript knows data exists
case 'error':
return `Error: ${state.error.message}`; // TypeScript knows error exists
}
}
Make your generics more powerful:
// Constrain to objects with an id property
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Usage
const users = [{ id: '1', name: 'John' }, { id: '2', name: 'Jane' }];
const user = findById(users, '1'); // Type: { id: string; name: string } | undefined
Create dynamic types based on conditions:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical example: Extract promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type Result = Awaited<Promise<string>>; // string
Build powerful string types:
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${Endpoint}`;
// Results in: 'GET /users' | 'GET /posts' | 'GET /comments' | 'POST /users' | ...
// Route handler
function handleRequest(route: APIRoute) {
// Type-safe routing
}
Prevent mixing similar primitive types:
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUserById(id: UserId) {
// Fetch user
}
const userId = createUserId('user-123');
const productId = 'product-456' as ProductId;
getUserById(userId); // ✅ Works
getUserById(productId); // ❌ Type error!
Type-safe error handling:
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number
) {
super(message);
this.name = 'AppError';
}
}
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise<Result<User, AppError>> {
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return { success: true, value: user };
} catch (error) {
return {
success: false,
error: new AppError('Failed to fetch user', 'FETCH_ERROR', 500)
};
}
}
Always use strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}
const assertions for literal types:const routes = ['/', '/about', '/contact'] as const;
type Route = typeof routes[number]; // '/' | '/about' | '/contact'
// Consider splitting complex types
type ComplexType = Omit<Pick<User, 'id' | 'name'>, never> & { metadata: unknown };
// Better: Create intermediate types
type UserBasics = Pick<User, 'id' | 'name'>;
type ComplexType = UserBasics & { metadata: unknown };
import { expectType } from 'tsd';
// Type-level tests
expectType<string>(getUserName());
expectType<number>(getUserAge());
Mastering TypeScript's advanced features enables you to build robust, maintainable applications. Start incorporating these patterns into your codebase today!
any, use unknown when type is truly unknown