TypeScript has become the industry standard for building large-scale JavaScript applications. But there's a significant difference between using TypeScript and using it well. In this guide, I'll share the patterns and practices that have helped me build more maintainable, bug-free applications.
Start with Strict Mode
The most important TypeScript configuration is enabling strict mode. This activates a suite of checks that catch common errors and enforce best practices. Don't start a new project without it.
json
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"esModuleInterop": true
}
}Mastering Utility Types
TypeScript's built-in utility types are incredibly powerful. Understanding them eliminates the need for most custom type definitions and makes your code more readable.
typescript
// Essential utility types
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// Partial - all properties optional
type UpdateUserDTO = Partial<User>;
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'email'>;
// Omit - exclude specific properties
type PublicUser = Omit<User, 'password'>;
// Required - make all properties required
type RequiredUser = Required<Partial<User>>;
// Record - create object type with specific keys
type UserRoles = Record<'admin' | 'user' | 'guest', boolean>;
// Extract & Exclude - filter union types
type StringOrNumber = string | number | boolean;
type OnlyStrings = Extract<StringOrNumber, string>; // string
type NoStrings = Exclude<StringOrNumber, string>; // number | booleanGeneric Patterns
Generics are TypeScript's most powerful feature. They enable you to write reusable, type-safe code that works with any type while maintaining full type inference.
typescript
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
timestamp: Date;
}
// Generic fetch function
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return {
data,
success: response.ok,
timestamp: new Date()
};
}
// Usage - TypeScript infers the type
const users = await fetchData<User[]>('/api/users');
// users.data is typed as User[]
// Generic with constraints
interface HasId {
id: string | number;
}
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
return items.find(item => item.id === id);
}
// Works with any type that has an id property
const user = findById(users.data, '123'); // Fully typed!Type Guards and Narrowing
Type guards help TypeScript understand your code's control flow. Use them to narrow types and avoid unsafe type assertions.
typescript
// Custom type guard
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.email);
}
}
// Discriminated unions - the best pattern for variants
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function handleResult<T>(result: Result<T>) {
if (result.success) {
// TypeScript knows result.data exists
return result.data;
} else {
// TypeScript knows result.error exists
throw result.error;
}
}React with TypeScript
TypeScript and React are a perfect match. Here are the patterns that make React development type-safe and enjoyable.
typescript
// Component props with children
interface CardProps {
title: string;
className?: string;
children: React.ReactNode;
}
// Generic component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>Common Anti-Patterns to Avoid
- Don't use 'any' - use 'unknown' and type guards instead
- Avoid type assertions (as Type) - narrow types with guards
- Don't export types from .ts files - use .d.ts for shared types
- Avoid optional chaining abuse (?.) - fix your types instead
- Don't ignore TypeScript errors - they're trying to help you
40%Fewer bugs
2xFaster refactoring
100%Type coverage goal
0'any' types allowed