π― Introduction
TypeScript has revolutionized JavaScript development by bringing static typing and advanced tooling to the ecosystem. However, leveraging TypeScript’s full potential requires understanding not just the syntax, but the principles and patterns that lead to maintainable, type-safe code.
This comprehensive guide explores TypeScript best practices across multiple dimensions:
- Configuration & Setup: Optimal compiler settings and project structure
- Type System Mastery: Leveraging TypeScript’s powerful type system effectively
- Code Style & Syntax: Consistent, readable, and idiomatic TypeScript code
- Design Patterns: Applying proven patterns in a type-safe manner
- Advanced Techniques: Generics, utility types, and type transformations
- Performance & Optimization: Writing efficient TypeScript code
- Testing & Quality: Ensuring type safety extends to your test suite
π‘ Core Philosophy: “TypeScript is not just JavaScript with typesβit’s a tool for designing robust APIs, catching bugs early, and enabling confident refactoring”
π TypeScript Configuration Best Practices
π§ Essential tsconfig.json Settings
A properly configured tsconfig.json is the foundation of a type-safe TypeScript project.
π οΈ Strict Configuration (Recommended)
1{
2 "compilerOptions": {
3 // Strict Type Checking
4 "strict": true, // Enable all strict type checking options
5 "noImplicitAny": true, // Raise error on expressions with implied 'any'
6 "strictNullChecks": true, // Enable strict null checks
7 "strictFunctionTypes": true, // Enable strict checking of function types
8 "strictBindCallApply": true, // Enable strict bind/call/apply methods
9 "strictPropertyInitialization": true, // Ensure properties are initialized
10 "noImplicitThis": true, // Raise error on 'this' with implied 'any'
11 "alwaysStrict": true, // Parse in strict mode and emit "use strict"
12
13 // Additional Checks
14 "noUnusedLocals": true, // Report errors on unused locals
15 "noUnusedParameters": true, // Report errors on unused parameters
16 "noImplicitReturns": true, // Report error when not all paths return value
17 "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases
18
19 // Module Resolution
20 "module": "ESNext", // Specify module code generation
21 "moduleResolution": "node", // Use Node.js module resolution
22 "resolveJsonModule": true, // Include modules imported with .json
23 "esModuleInterop": true, // Enable interop between CommonJS and ES Modules
24 "allowSyntheticDefaultImports": true, // Allow default imports from modules
25
26 // Emit
27 "target": "ES2020", // Specify ECMAScript target version
28 "lib": ["ES2020", "DOM", "DOM.Iterable"], // Specify library files
29 "outDir": "./dist", // Redirect output structure to directory
30 "sourceMap": true, // Generate source maps
31 "declaration": true, // Generate .d.ts files
32 "declarationMap": true, // Generate sourcemap for .d.ts files
33 "removeComments": false, // Keep comments in output
34
35 // Interop Constraints
36 "isolatedModules": true, // Ensure each file can be transpiled
37 "allowJs": false, // Disallow JavaScript files
38 "checkJs": false, // Don't check JavaScript files
39
40 // Advanced
41 "skipLibCheck": true, // Skip type checking of declaration files
42 "forceConsistentCasingInFileNames": true, // Ensure consistent casing
43 "incremental": true, // Enable incremental compilation
44 "tsBuildInfoFile": "./dist/.tsbuildinfo" // Specify file for incremental info
45 },
46 "include": ["src/**/*"],
47 "exclude": ["node_modules", "dist", "**/*.spec.ts"]
48}
π Configuration Strategy by Project Type
graph TD
A[Project Type] --> B[Library]
A --> C[Application]
A --> D[Monorepo]
B --> B1[declaration: true]
B --> B2[declarationMap: true]
B --> B3[Remove comments: false]
C --> C1[sourceMap: true]
C --> C2[optimization flags]
C --> C3[Runtime-specific lib]
D --> D1[composite: true]
D --> D2[Project references]
D --> D3[Shared base config]
style A fill:#ff6b6b
style B fill:#4ecdc4
style C fill:#feca57
style D fill:#95e1d3
ποΈ Project Structure Best Practices
project-root/
βββ src/
β βββ types/ # Shared type definitions
β β βββ index.ts
β β βββ models.ts
β βββ utils/ # Utility functions
β β βββ validation.ts
β β βββ formatting.ts
β βββ services/ # Business logic
β β βββ api.service.ts
β βββ components/ # UI components (if applicable)
β βββ index.ts # Main entry point
βββ tests/
β βββ unit/
β βββ integration/
βββ tsconfig.json # Base TypeScript config
βββ tsconfig.build.json # Production build config
βββ package.json
π¨ Type System Best Practices
β Prefer Types Over Interfaces (When Appropriate)
1// β
GOOD: Use type for unions, intersections, and primitives
2type Status = 'pending' | 'success' | 'error';
3type ID = string | number;
4
5type Point = {
6 x: number;
7 y: number;
8};
9
10// Intersection types
11type ColoredPoint = Point & {
12 color: string;
13};
14
15// β
GOOD: Use interface for object shapes that might be extended
16interface User {
17 id: string;
18 name: string;
19 email: string;
20}
21
22interface AdminUser extends User {
23 permissions: string[];
24}
25
26// β AVOID: Using interface for union types (not possible)
27// interface Status = 'pending' | 'success' | 'error'; // Error!
Key Differences:
1// Type aliases can represent any type
2type StringOrNumber = string | number;
3type Tuple = [string, number];
4type Callback = (data: string) => void;
5
6// Interfaces are for object shapes and classes
7interface Animal {
8 name: string;
9 makeSound(): void;
10}
11
12// Interfaces support declaration merging
13interface Window {
14 customProperty: string;
15}
16
17interface Window {
18 anotherProperty: number;
19}
20// Both declarations merge into one
π Embrace Strict Null Checks
1// β
GOOD: Explicit handling of null/undefined
2function getUserName(user: User | null): string {
3 if (user === null) {
4 return 'Guest';
5 }
6 return user.name;
7}
8
9// β
GOOD: Optional chaining
10function getAddressCity(user?: User): string | undefined {
11 return user?.address?.city;
12}
13
14// β
GOOD: Nullish coalescing
15const displayName = user?.name ?? 'Anonymous';
16
17// β AVOID: Non-null assertion (use sparingly)
18function processUser(user: User | null) {
19 console.log(user!.name); // Dangerous! Runtime error if null
20}
21
22// β
BETTER: Type guard
23function processUser(user: User | null) {
24 if (!user) {
25 throw new Error('User cannot be null');
26 }
27 console.log(user.name); // Safe: TypeScript knows user is not null
28}
π― Type Guards and Narrowing
1// β
GOOD: User-defined type guards
2interface Dog {
3 type: 'dog';
4 bark(): void;
5}
6
7interface Cat {
8 type: 'cat';
9 meow(): void;
10}
11
12type Animal = Dog | Cat;
13
14// Type predicate
15function isDog(animal: Animal): animal is Dog {
16 return animal.type === 'dog';
17}
18
19function handleAnimal(animal: Animal) {
20 if (isDog(animal)) {
21 animal.bark(); // TypeScript knows it's a Dog
22 } else {
23 animal.meow(); // TypeScript knows it's a Cat
24 }
25}
26
27// β
GOOD: Discriminated unions
28type Shape =
29 | { kind: 'circle'; radius: number }
30 | { kind: 'rectangle'; width: number; height: number }
31 | { kind: 'square'; size: number };
32
33function getArea(shape: Shape): number {
34 switch (shape.kind) {
35 case 'circle':
36 return Math.PI * shape.radius ** 2;
37 case 'rectangle':
38 return shape.width * shape.height;
39 case 'square':
40 return shape.size ** 2;
41 default:
42 // Exhaustiveness check
43 const _exhaustive: never = shape;
44 throw new Error(`Unhandled shape: ${_exhaustive}`);
45 }
46}
π§© Generics Best Practices
1// β
GOOD: Generic function with constraints
2function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
3 return obj[key];
4}
5
6const user = { name: 'John', age: 30 };
7const name = getProperty(user, 'name'); // Type: string
8const age = getProperty(user, 'age'); // Type: number
9
10// β
GOOD: Generic interface with defaults
11interface ApiResponse<T = unknown> {
12 data: T;
13 status: number;
14 message: string;
15}
16
17const userResponse: ApiResponse<User> = {
18 data: { id: '1', name: 'John', email: 'john@example.com' },
19 status: 200,
20 message: 'Success'
21};
22
23// β
GOOD: Conditional types
24type Unwrap<T> = T extends Promise<infer U> ? U : T;
25
26type A = Unwrap<Promise<string>>; // string
27type B = Unwrap<number>; // number
28
29// β
GOOD: Mapped types
30type Readonly<T> = {
31 readonly [P in keyof T]: T[P];
32};
33
34type Partial<T> = {
35 [P in keyof T]?: T[P];
36};
37
38// β AVOID: Overly complex generics
39type ComplexGeneric<T extends Record<string, unknown>, K extends keyof T, V extends T[K]> = {
40 [P in K]: V;
41};
42
43// β
BETTER: Break it down
44type SimpleValue<T, K extends keyof T> = T[K];
π Code Style and Syntax Best Practices
π Naming Conventions
1// β
GOOD: Clear, descriptive names
2// Interfaces and Types: PascalCase
3interface UserProfile { }
4type RequestStatus = 'pending' | 'success' | 'error';
5
6// Classes: PascalCase
7class UserService { }
8
9// Functions and variables: camelCase
10function calculateTotal(items: Item[]): number { }
11const userName = 'John';
12
13// Constants: UPPER_SNAKE_CASE
14const MAX_RETRY_ATTEMPTS = 3;
15const API_BASE_URL = 'https://api.example.com';
16
17// Enums: PascalCase for enum, UPPER_CASE for members
18enum HttpStatus {
19 OK = 200,
20 NOT_FOUND = 404,
21 INTERNAL_SERVER_ERROR = 500
22}
23
24// Private members: prefix with underscore (optional)
25class User {
26 private _password: string;
27
28 constructor(password: string) {
29 this._password = password;
30 }
31}
32
33// Boolean variables: use is/has/should prefix
34const isLoading = true;
35const hasPermission = false;
36const shouldRetry = true;
π― Function Best Practices
1// β
GOOD: Explicit return types
2function calculatePrice(quantity: number, unitPrice: number): number {
3 return quantity * unitPrice;
4}
5
6// β
GOOD: Use function overloads for different signatures
7function createElement(tag: 'div'): HTMLDivElement;
8function createElement(tag: 'span'): HTMLSpanElement;
9function createElement(tag: string): HTMLElement;
10function createElement(tag: string): HTMLElement {
11 return document.createElement(tag);
12}
13
14// β
GOOD: Optional parameters come last
15function greet(name: string, greeting?: string): string {
16 return `${greeting ?? 'Hello'}, ${name}!`;
17}
18
19// β
GOOD: Use destructuring with types
20interface UserOptions {
21 name: string;
22 age?: number;
23 email?: string;
24}
25
26function createUser({ name, age = 18, email }: UserOptions): User {
27 return { name, age, email: email ?? '' };
28}
29
30// β
GOOD: Arrow functions for callbacks
31const numbers = [1, 2, 3, 4, 5];
32const doubled = numbers.map((n) => n * 2);
33const evenNumbers = numbers.filter((n) => n % 2 === 0);
34
35// β AVOID: Implicit any parameters
36function process(data) { // Error: Parameter 'data' implicitly has an 'any' type
37 console.log(data);
38}
39
40// β
GOOD: Typed parameters
41function process(data: unknown): void {
42 console.log(data);
43}
π·οΈ Enum vs Union Types
1// β
GOOD: Use const enum for compile-time constants
2const enum Direction {
3 Up,
4 Down,
5 Left,
6 Right
7}
8
9const direction: Direction = Direction.Up;
10
11// β
GOOD: Use string literal unions for flexibility
12type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
13
14function request(url: string, method: HttpMethod): Promise<Response> {
15 return fetch(url, { method });
16}
17
18// β
GOOD: Use as const for readonly literal types
19const ROUTES = {
20 HOME: '/',
21 ABOUT: '/about',
22 CONTACT: '/contact'
23} as const;
24
25type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/about' | '/contact'
26
27// β AVOID: Regular enums if string unions suffice
28enum Status {
29 Pending = 'PENDING',
30 Success = 'SUCCESS',
31 Error = 'ERROR'
32}
33
34// β
BETTER: String union
35type Status = 'PENDING' | 'SUCCESS' | 'ERROR';
ποΈ Design Patterns in TypeScript
π Singleton Pattern (Type-Safe)
1// β
GOOD: Type-safe singleton with private constructor
2class DatabaseConnection {
3 private static instance: DatabaseConnection;
4 private connectionString: string;
5
6 private constructor(connectionString: string) {
7 this.connectionString = connectionString;
8 }
9
10 public static getInstance(connectionString?: string): DatabaseConnection {
11 if (!DatabaseConnection.instance) {
12 if (!connectionString) {
13 throw new Error('Connection string required for first initialization');
14 }
15 DatabaseConnection.instance = new DatabaseConnection(connectionString);
16 }
17 return DatabaseConnection.instance;
18 }
19
20 public query<T>(sql: string): Promise<T[]> {
21 // Implementation
22 return Promise.resolve([]);
23 }
24}
25
26// Usage
27const db = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');
28const users = await db.query<User>('SELECT * FROM users');
ποΈ Factory Pattern with Generics
1// β
GOOD: Generic factory pattern
2interface Product {
3 id: string;
4 name: string;
5 price: number;
6}
7
8interface Service {
9 id: string;
10 name: string;
11 duration: number;
12}
13
14interface Factory<T> {
15 create(data: Omit<T, 'id'>): T;
16}
17
18class ProductFactory implements Factory<Product> {
19 private idCounter = 0;
20
21 create(data: Omit<Product, 'id'>): Product {
22 return {
23 id: `product-${++this.idCounter}`,
24 ...data
25 };
26 }
27}
28
29class ServiceFactory implements Factory<Service> {
30 private idCounter = 0;
31
32 create(data: Omit<Service, 'id'>): Service {
33 return {
34 id: `service-${++this.idCounter}`,
35 ...data
36 };
37 }
38}
39
40// Usage
41const productFactory = new ProductFactory();
42const product = productFactory.create({ name: 'Laptop', price: 1200 });
43
44const serviceFactory = new ServiceFactory();
45const service = serviceFactory.create({ name: 'Consultation', duration: 60 });
π Strategy Pattern with Type Safety
1// β
GOOD: Type-safe strategy pattern
2interface PaymentStrategy {
3 pay(amount: number): Promise<PaymentResult>;
4}
5
6interface PaymentResult {
7 success: boolean;
8 transactionId?: string;
9 error?: string;
10}
11
12class CreditCardPayment implements PaymentStrategy {
13 constructor(
14 private cardNumber: string,
15 private cvv: string,
16 private expiryDate: string
17 ) {}
18
19 async pay(amount: number): Promise<PaymentResult> {
20 // Implementation
21 return {
22 success: true,
23 transactionId: `cc-${Date.now()}`
24 };
25 }
26}
27
28class PayPalPayment implements PaymentStrategy {
29 constructor(private email: string, private password: string) {}
30
31 async pay(amount: number): Promise<PaymentResult> {
32 // Implementation
33 return {
34 success: true,
35 transactionId: `pp-${Date.now()}`
36 };
37 }
38}
39
40class PaymentProcessor {
41 constructor(private strategy: PaymentStrategy) {}
42
43 setStrategy(strategy: PaymentStrategy): void {
44 this.strategy = strategy;
45 }
46
47 async processPayment(amount: number): Promise<PaymentResult> {
48 return this.strategy.pay(amount);
49 }
50}
51
52// Usage
53const processor = new PaymentProcessor(
54 new CreditCardPayment('1234-5678-9012-3456', '123', '12/25')
55);
56await processor.processPayment(100);
57
58processor.setStrategy(new PayPalPayment('user@example.com', 'password'));
59await processor.processPayment(50);
π Observer Pattern with Strong Typing
1// β
GOOD: Type-safe observer pattern
2type Observer<T> = (data: T) => void;
3
4class Observable<T> {
5 private observers: Set<Observer<T>> = new Set();
6
7 subscribe(observer: Observer<T>): () => void {
8 this.observers.add(observer);
9
10 // Return unsubscribe function
11 return () => {
12 this.observers.delete(observer);
13 };
14 }
15
16 notify(data: T): void {
17 this.observers.forEach((observer) => observer(data));
18 }
19}
20
21// Usage with specific types
22interface UserEvent {
23 type: 'login' | 'logout';
24 userId: string;
25 timestamp: number;
26}
27
28const userEvents = new Observable<UserEvent>();
29
30const unsubscribe = userEvents.subscribe((event) => {
31 console.log(`User ${event.userId} performed ${event.type}`);
32});
33
34userEvents.notify({
35 type: 'login',
36 userId: 'user-123',
37 timestamp: Date.now()
38});
39
40unsubscribe(); // Clean up
π Advanced TypeScript Techniques
π§ Utility Types Mastery
1// β
GOOD: Leverage built-in utility types
2interface User {
3 id: string;
4 name: string;
5 email: string;
6 password: string;
7 role: 'admin' | 'user';
8}
9
10// Partial: All properties optional
11type UserUpdate = Partial<User>;
12
13// Pick: Select specific properties
14type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
15
16// Omit: Exclude specific properties
17type UserWithoutPassword = Omit<User, 'password'>;
18
19// Required: All properties required
20type UserRequired = Required<Partial<User>>;
21
22// Readonly: All properties readonly
23type UserImmutable = Readonly<User>;
24
25// Record: Create object type with specific keys
26type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
27
28// β
GOOD: Custom utility types
29type DeepPartial<T> = {
30 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
31};
32
33type DeepReadonly<T> = {
34 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
35};
36
37// Make specific keys optional
38type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
39
40type UserOptionalEmail = OptionalKeys<User, 'email'>;
41
42// Make specific keys required
43type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
π¨ Template Literal Types
1// β
GOOD: Template literal types for type-safe strings
2type EventName = 'click' | 'focus' | 'blur';
3type ElementId = 'button' | 'input' | 'form';
4
5type ElementEvent = `${ElementId}:${EventName}`;
6// Result: 'button:click' | 'button:focus' | 'button:blur' | 'input:click' | ...
7
8// β
GOOD: Type-safe API routes
9type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
10type ApiVersion = 'v1' | 'v2';
11type Resource = 'users' | 'posts' | 'comments';
12
13type ApiRoute = `/${ApiVersion}/${Resource}`;
14type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
15
16const endpoint: ApiEndpoint = 'GET /v1/users'; // Type-safe!
17
18// β
GOOD: CSS-in-JS type safety
19type CSSUnit = 'px' | 'em' | 'rem' | '%';
20type Size = `${number}${CSSUnit}`;
21
22interface Style {
23 width: Size;
24 height: Size;
25 margin: Size;
26}
27
28const style: Style = {
29 width: '100px',
30 height: '50rem',
31 margin: '10%'
32};
π Branded Types for Type Safety
1// β
GOOD: Branded types prevent mixing incompatible values
2type Brand<K, T> = K & { __brand: T };
3
4type UserId = Brand<string, 'UserId'>;
5type PostId = Brand<string, 'PostId'>;
6type Email = Brand<string, 'Email'>;
7
8// Factory functions for creating branded types
9function createUserId(id: string): UserId {
10 return id as UserId;
11}
12
13function createPostId(id: string): PostId {
14 return id as PostId;
15}
16
17function createEmail(email: string): Email {
18 if (!email.includes('@')) {
19 throw new Error('Invalid email format');
20 }
21 return email as Email;
22}
23
24// Type-safe functions
25function getUserById(userId: UserId): User | null {
26 // Implementation
27 return null;
28}
29
30function getPostById(postId: PostId): Post | null {
31 // Implementation
32 return null;
33}
34
35// Usage
36const userId = createUserId('user-123');
37const postId = createPostId('post-456');
38
39getUserById(userId); // β
OK
40getUserById(postId); // β Error: Type 'PostId' is not assignable to type 'UserId'
π§ͺ Conditional Types and Inference
1// β
GOOD: Extract return type
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4function getUser(): User {
5 return { id: '1', name: 'John', email: 'john@example.com' };
6}
7
8type UserType = ReturnType<typeof getUser>; // User
9
10// β
GOOD: Extract array element type
11type ArrayElement<T> = T extends (infer E)[] ? E : never;
12
13type StringArray = string[];
14type Element = ArrayElement<StringArray>; // string
15
16// β
GOOD: Flatten promise types
17type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
18
19type NestedPromise = Promise<Promise<User>>;
20type FlattenedUser = Awaited<NestedPromise>; // User
21
22// β
GOOD: Function parameter types
23type Parameters<T> = T extends (...args: infer P) => any ? P : never;
24
25function createUser(name: string, age: number, email: string): User {
26 return { id: '1', name, email };
27}
28
29type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]
π§ͺ Testing Best Practices
β Type-Safe Test Setup
1// β
GOOD: Type-safe test data factories
2interface User {
3 id: string;
4 name: string;
5 email: string;
6 age: number;
7}
8
9function createMockUser(overrides?: Partial<User>): User {
10 return {
11 id: 'test-id',
12 name: 'Test User',
13 email: 'test@example.com',
14 age: 25,
15 ...overrides
16 };
17}
18
19// β
GOOD: Type-safe mocks with Jest
20import { jest } from '@jest/globals';
21
22interface UserService {
23 getUser(id: string): Promise<User>;
24 updateUser(id: string, data: Partial<User>): Promise<User>;
25}
26
27const mockUserService: jest.Mocked<UserService> = {
28 getUser: jest.fn(),
29 updateUser: jest.fn()
30};
31
32// Type-safe mock implementation
33mockUserService.getUser.mockResolvedValue(createMockUser());
34
35// β
GOOD: Type assertions in tests
36describe('User Service', () => {
37 it('should return user data', async () => {
38 const user = await userService.getUser('123');
39
40 // Type-safe assertions
41 expect(user).toMatchObject<Partial<User>>({
42 id: '123',
43 name: expect.any(String),
44 email: expect.any(String)
45 });
46 });
47});
β‘ Performance and Optimization
π― Type-Level Performance
1// β AVOID: Deep type recursion
2type DeepNested<T, N extends number> = N extends 0
3 ? T
4 : DeepNested<T[], Subtract<N, 1>>;
5
6// β
GOOD: Limit recursion depth
7type DeepPartial<T> = T extends object
8 ? { [P in keyof T]?: DeepPartial<T[P]> }
9 : T;
10
11// β AVOID: Excessive union types
12type AllCombinations =
13 | Type1 | Type2 | Type3 | Type4 | Type5
14 | Type6 | Type7 | Type8 | Type9 | Type10; // ... 100 more
15
16// β
GOOD: Use discriminated unions
17type Action =
18 | { type: 'INCREMENT'; payload: number }
19 | { type: 'DECREMENT'; payload: number }
20 | { type: 'RESET' };
π Runtime Performance
1// β
GOOD: Use const assertions to avoid runtime overhead
2const config = {
3 apiUrl: 'https://api.example.com',
4 timeout: 5000,
5 retries: 3
6} as const;
7
8// Type is: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3 }
9
10// β
GOOD: Avoid unnecessary type casting
11function processData(data: unknown): string {
12 // Type guard instead of casting
13 if (typeof data === 'string') {
14 return data.toUpperCase();
15 }
16 return String(data);
17}
18
19// β AVOID: Excessive type assertions
20function process(value: unknown): string {
21 return (value as any).toString(); // Loses type safety
22}
π¨ Common Pitfalls and How to Avoid Them
β οΈ Pitfall 1: Type Assertions vs Type Guards
1// β BAD: Unsafe type assertion
2function processUser(data: unknown) {
3 const user = data as User; // No runtime check!
4 console.log(user.name.toUpperCase()); // May crash
5}
6
7// β
GOOD: Type guard with validation
8function isUser(data: unknown): data is User {
9 return (
10 typeof data === 'object' &&
11 data !== null &&
12 'id' in data &&
13 'name' in data &&
14 'email' in data
15 );
16}
17
18function processUser(data: unknown) {
19 if (!isUser(data)) {
20 throw new Error('Invalid user data');
21 }
22 console.log(data.name.toUpperCase()); // Safe!
23}
β οΈ Pitfall 2: Any vs Unknown
1// β BAD: Using 'any' loses type safety
2function processData(data: any): string {
3 return data.toString(); // No type checking
4}
5
6// β
GOOD: Use 'unknown' and narrow the type
7function processData(data: unknown): string {
8 if (typeof data === 'string') {
9 return data;
10 }
11 if (typeof data === 'number') {
12 return String(data);
13 }
14 throw new Error('Unsupported data type');
15}
β οΈ Pitfall 3: Optional vs Undefined
1// β CONFUSING: Mixing optional and undefined
2interface User {
3 name?: string | undefined; // Redundant
4 email?: string; // Better
5}
6
7// β
GOOD: Be explicit about intent
8interface Config {
9 timeout?: number; // Can be omitted
10 retries: number | undefined; // Must be provided, can be undefined
11}
12
13const config1: Config = { retries: undefined }; // β
OK
14const config2: Config = { retries: 3 }; // β
OK
15const config3: Config = {}; // β Error: retries is required
β οΈ Pitfall 4: Array Methods and Type Narrowing
1// β BAD: Filter doesn't narrow types automatically
2const values: (string | null)[] = ['a', null, 'b', null];
3const strings = values.filter(v => v !== null);
4// Type is still (string | null)[]
5
6// β
GOOD: Use type predicate
7function isNotNull<T>(value: T | null): value is T {
8 return value !== null;
9}
10
11const strings = values.filter(isNotNull); // Type is string[]
π Summary and Quick Reference
π― Key Principles
- Enable Strict Mode: Always use
"strict": truein tsconfig.json - Prefer Type Safety: Choose
unknownoverany, use type guards instead of assertions - Leverage the Type System: Use discriminated unions, branded types, and utility types
- Be Explicit: Add return types to functions, use const assertions
- Design for Maintainability: Use clear naming, consistent patterns, and proper structure
π οΈ Essential Commands
1# Initialize TypeScript project
2npm init -y
3npm install -D typescript @types/node
4
5# Create tsconfig.json
6npx tsc --init --strict
7
8# Type check without emitting
9npx tsc --noEmit
10
11# Watch mode
12npx tsc --watch
13
14# Build project
15npx tsc
16
17# Run TypeScript directly (development)
18npx tsx src/index.ts
π Recommended Tools
1{
2 "devDependencies": {
3 "typescript": "^5.3.0",
4 "@typescript-eslint/eslint-plugin": "^6.0.0",
5 "@typescript-eslint/parser": "^6.0.0",
6 "eslint": "^8.0.0",
7 "prettier": "^3.0.0",
8 "ts-node": "^10.0.0",
9 "tsx": "^4.0.0",
10 "@types/node": "^20.0.0",
11 "jest": "^29.0.0",
12 "@types/jest": "^29.0.0",
13 "ts-jest": "^29.0.0"
14 }
15}
π¨ ESLint Configuration for TypeScript
1{
2 "parser": "@typescript-eslint/parser",
3 "extends": [
4 "eslint:recommended",
5 "plugin:@typescript-eslint/recommended",
6 "plugin:@typescript-eslint/recommended-requiring-type-checking"
7 ],
8 "parserOptions": {
9 "project": "./tsconfig.json"
10 },
11 "rules": {
12 "@typescript-eslint/no-explicit-any": "error",
13 "@typescript-eslint/explicit-function-return-type": "warn",
14 "@typescript-eslint/no-unused-vars": "error",
15 "@typescript-eslint/no-non-null-assertion": "warn"
16 }
17}
π Further Learning Resources
- Official Documentation: TypeScript Handbook
- Advanced Types: TypeScript Deep Dive
- Type Challenges: type-challenges
- Style Guide: Google TypeScript Style Guide
π― Conclusion
TypeScript is a powerful tool that, when used correctly, dramatically improves code quality, maintainability, and developer experience. By following these best practices, you’ll write type-safe code that catches bugs early, enables confident refactoring, and scales with your application’s growth.
Remember: TypeScript is not just about adding typesβit’s about designing better APIs, expressing intent clearly, and leveraging the compiler as a powerful ally in your development workflow.
Related Posts:
- Essential Design Patterns in Java: A Comprehensive Guide
- Java Concurrency Deep Dive: Runnable and Callable Patterns
- Building Advanced MCP Servers with Claude Code - Part 2
Tags: #TypeScript #JavaScript #TypeSafety #DesignPatterns #BestPractices #SoftwareEngineering #WebDevelopment #FrontendDevelopment