TypeScript Expert
Advanced TypeScript patterns, generics, and type safety best practices
# TypeScript Expert Best Practices
Comprehensive guide for mastering TypeScript's advanced type system, generics, and building type-safe applications at scale.
---
## Core TypeScript Principles
1. **Strict Type Safety**
- Enable strict mode and all strict compiler options
- Minimize use of `any` type - prefer `unknown` when type is uncertain
- Use type assertions sparingly and with proper validation
- Example strict configuration:
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
```
2. **Type vs Interface Design**
- Use `interface` for object shapes that might be extended
- Use `type` for unions, primitives, computed types, and complex type operations
- Prefer composition over inheritance for complex type relationships
- Example usage patterns:
```ts
// Use interface for extensible object shapes
interface User {
id: string;
name: string;
email: string;
}
interface AdminUser extends User {
permissions: string[];
}
// Use type for unions and computed types
type Status = 'pending' | 'approved' | 'rejected';
type UserWithStatus = User & { status: Status };
```
3. **Leveraging Type Inference**
- Let TypeScript infer types when they're obvious
- Explicitly type function return values for clarity and error catching
- Use const assertions for immutable data structures
- Example inference patterns:
```ts
// Good: Let TypeScript infer simple types
const numbers = [1, 2, 3]; // inferred as number[]
const user = { name: 'John', age: 30 }; // inferred shape
// Good: Explicit return types for functions
function getUser(id: string): Promise<User | null> {
return database.findUser(id);
}
// Good: Const assertions for literal types
const themes = ['light', 'dark'] as const;
type Theme = typeof themes[number]; // 'light' | 'dark'
```
---
## Advanced Type Patterns
4. **Generic Types with Constraints**
- Use generic constraints to limit type parameters
- Implement proper bounds checking for type safety
- Create reusable generic utilities
- Example generic patterns:
```ts
// Generic with constraints
interface HasId {
id: string;
}
function updateEntity<T extends HasId>(entity: T, updates: Partial<T>): T {
return { ...entity, ...updates };
}
// Multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Generic with default types
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
```
5. **Conditional Types and Mapped Types**
- Use conditional types for type-level logic
- Create mapped types for transforming existing types
- Implement advanced type utilities
- Example conditional types:
```ts
// Conditional type for extracting function return types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Conditional type for filtering
type NonNullable<T> = T extends null | undefined ? never : T;
// Mapped type for making properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Deep readonly utility
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
```
6. **Template Literal Types**
- Create dynamic string types with template literals
- Build type-safe APIs with string manipulation
- Generate types from string patterns
- Example template literal types:
```ts
// Event system with template literals
type EventName = 'click' | 'focus' | 'blur';
type ElementId = 'button' | 'input' | 'form';
type EventHandler = `on${Capitalize<EventName>}`;
type ElementEvent = `${ElementId}:${EventName}`;
// Type-safe CSS-in-JS
type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type DetailedCSSProperty = `${CSSProperty}-${CSSDirection}`;
// API endpoint types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/api/${string}`;
type ApiCall = `${HttpMethod} ${Endpoint}`;
```
---
## Utility Types and Type Manipulation
7. **Built-in Utility Types**
- Master essential utility types: Partial, Required, Pick, Omit, Record
- Use ReturnType, Parameters, and ConstructorParameters for function types
- Leverage Exclude, Extract, and NonNullable for type filtering
- Example utility type usage:
```ts
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Pick only safe fields for public API
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Create update type without id and createdAt
type UserUpdate = Omit<User, 'id' | 'createdAt'>;
// Make all fields optional for partial updates
type UserPartialUpdate = Partial<UserUpdate>;
// Extract function types
type AsyncFunction<T> = (...args: any[]) => Promise<T>;
type ExtractPromiseType<T> = T extends Promise<infer U> ? U : never;
```
8. **Custom Utility Types**
- Create project-specific utility types for common patterns
- Build composable type transformations
- Implement domain-specific type helpers
- Example custom utilities:
```ts
// Make specific fields required
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Create a type with all string values
type Stringify<T> = {
[K in keyof T]: string;
};
// Extract keys by value type
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
// Function overload helper
type Overload<T> = T extends {
(...args: infer A1): infer R1;
(...args: infer A2): infer R2;
} ? ((...args: A1) => R1) & ((...args: A2) => R2) : never;
```
---
## Type Guards and Validation
9. **Type Guards and Predicates**
- Implement type guards for runtime type checking
- Use assertion functions for validation
- Create branded types for type safety
- Example type guard patterns:
```ts
// Basic type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Object type guard
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj;
}
// Assertion function
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new Error('Expected number');
}
}
// Branded types for type safety
type UserId = string & { readonly brand: unique symbol };
type Email = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
// Validation logic here
return id as UserId;
}
```
10. **Discriminated Unions**
- Use discriminated unions for type-safe state management
- Implement exhaustiveness checking with never type
- Create type-safe reducers and state machines
- Example discriminated union patterns:
```ts
// State machine with discriminated unions
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: any }
| { status: 'error'; error: string };
function handleState(state: LoadingState) {
switch (state.status) {
case 'idle':
return 'No data loaded';
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${state.data}`;
case 'error':
return `Error: ${state.error}`;
default:
// Exhaustiveness check
const _exhaustive: never = state;
return _exhaustive;
}
}
// Action types for reducers
type Action =
| { type: 'SET_LOADING' }
| { type: 'SET_SUCCESS'; payload: any }
| { type: 'SET_ERROR'; payload: string };
```
---
## Advanced Patterns
11. **Higher-Order Types**
- Create types that operate on other types
- Build composable type transformations
- Implement functional programming patterns in types
- Example higher-order type patterns:
```ts
// Functor-like type transformation
type Functor<F> = {
map: <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B>;
};
// Option type implementation
type Option<T> = Some<T> | None;
type Some<T> = { readonly _tag: 'Some'; readonly value: T };
type None = { readonly _tag: 'None' };
// Result type for error handling
type Result<T, E> = Ok<T> | Err<E>;
type Ok<T> = { readonly _tag: 'Ok'; readonly value: T };
type Err<E> = { readonly _tag: 'Err'; readonly error: E };
// Composable validation
type Validator<T, E> = (input: unknown) => Result<T, E>;
```
12. **Module System and Namespaces**
- Use proper module organization and exports
- Implement namespace merging when appropriate
- Create type-only imports/exports
- Example module patterns:
```ts
// Type-only exports
export type { User, UserCreate, UserUpdate } from './types';
// Namespace for related types
namespace API {
export interface Request<T = unknown> {
data: T;
headers: Record<string, string>;
}
export interface Response<T = unknown> {
data: T;
status: number;
headers: Record<string, string>;
}
export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
}
// Declaration merging for extending libraries
declare global {
interface Window {
customProperty: string;
}
}
```
---
## Error Handling and Validation
13. **Type-Safe Error Handling**
- Use Result types instead of throwing exceptions
- Implement proper error type hierarchies
- Create composable error handling patterns
- Example error handling:
```ts
// Error type hierarchy
abstract class AppError {
abstract readonly name: string;
abstract readonly message: string;
}
class ValidationError extends AppError {
readonly name = 'ValidationError';
constructor(public readonly message: string, public readonly field: string) {
super();
}
}
class NetworkError extends AppError {
readonly name = 'NetworkError';
constructor(public readonly message: string, public readonly status: number) {
super();
}
}
// Result type for error handling
type AsyncResult<T, E extends AppError> = Promise<Result<T, E>>;
async function fetchUser(id: string): AsyncResult<User, NetworkError | ValidationError> {
try {
if (!id) {
return { _tag: 'Err', error: new ValidationError('ID is required', 'id') };
}
const user = await api.getUser(id);
return { _tag: 'Ok', value: user };
} catch (error) {
return { _tag: 'Err', error: new NetworkError('Failed to fetch', 500) };
}
}
```
14. **Runtime Validation with Types**
- Integrate runtime validation with TypeScript types
- Use libraries like Zod or io-ts for schema validation
- Ensure type safety at runtime boundaries
- Example validation patterns:
```ts
import { z } from 'zod';
// Schema that generates both runtime validation and types
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(0).max(120),
});
type User = z.infer<typeof UserSchema>;
// Type-safe API with validation
async function createUser(input: unknown): Promise<Result<User, ValidationError>> {
const parsed = UserSchema.safeParse(input);
if (!parsed.success) {
return {
_tag: 'Err',
error: new ValidationError('Invalid user data', parsed.error.message)
};
}
return { _tag: 'Ok', value: parsed.data };
}
```
---
## Performance and Optimization
15. **Type System Performance**
- Avoid deeply nested conditional types
- Use type aliases for complex types
- Implement proper type caching strategies
- Example performance patterns:
```ts
// Bad: Deeply nested conditional type
type BadDeepPick<T, K> = K extends keyof T
? K extends string
? K extends `${infer P}.${infer R}`
? { [Key in P]: BadDeepPick<T[P], R> }
: { [Key in K]: T[K] }
: never
: never;
// Good: Simplified with helper types
type Path<T> = T extends object
? { [K in keyof T]: K | `${K & string}.${Path<T[K]> & string}` }[keyof T]
: never;
type DeepPick<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}`
? K extends keyof T ? { [Key in K]: DeepPick<T[K], Rest> } : never
: P extends keyof T ? { [Key in P]: T[P] } : never;
```
16. **Compilation Optimization**
- Use project references for large codebases
- Implement incremental compilation
- Optimize import/export patterns
- Example optimization strategies:
```json
// tsconfig.json for large projects
{
"compilerOptions": {
"incremental": true,
"composite": true,
"skipLibCheck": true,
"declaration": true
},
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/utils" }
]
}
```
---
## Testing and Type Safety
17. **Type Testing**
- Write tests for your types using type-level assertions
- Use tools like `tsd` or `@typescript-eslint` for type testing
- Implement type-safe test helpers
- Example type testing:
```ts
// Type-level tests
type Assert<T extends true> = T;
type IsEqual<T, U> = T extends U ? U extends T ? true : false : false;
// Test type transformations
type Test1 = Assert<IsEqual<Pick<User, 'id'>, { id: string }>>;
type Test2 = Assert<IsEqual<keyof User, 'id' | 'name' | 'email'>>;
// Type-safe test utilities
function expectType<T>(_value: T): void {}
function expectError<T>(_fn: () => T): void {}
// Usage in tests
expectType<string>(user.name);
expectError(() => user.invalidProperty);
```
18. **Mocking with Type Safety**
- Create type-safe mocks for testing
- Use branded types for test data
- Implement proper mock type checking
- Example type-safe mocking:
```ts
// Type-safe mock factory
function createMock<T>(): { [K in keyof T]: jest.MockedFunction<T[K]> } {
return {} as any;
}
// Type-safe test data builder
class UserBuilder {
private user: Partial<User> = {};
withId(id: string): this {
this.user.id = id;
return this;
}
withName(name: string): this {
this.user.name = name;
return this;
}
build(): User {
return {
id: this.user.id ?? 'default-id',
name: this.user.name ?? 'Default Name',
email: this.user.email ?? 'default@example.com',
};
}
}
```
---
## Summary Checklist
- [ ] Enable strict TypeScript compiler options
- [ ] Use interfaces for extensible objects, types for unions
- [ ] Leverage type inference while being explicit where needed
- [ ] Master generic constraints and conditional types
- [ ] Implement type guards for runtime safety
- [ ] Use discriminated unions for state management
- [ ] Create custom utility types for common patterns
- [ ] Handle errors with Result types instead of exceptions
- [ ] Optimize type system performance for large codebases
- [ ] Write type-level tests for complex type logic
- [ ] Use branded types for domain-specific safety
- [ ] Implement proper module organization and exports
---
Follow these advanced TypeScript patterns to build robust, type-safe applications that scale effectively.