TypeScript Best Practices: Viết code type-safe và maintainable
Học cách viết TypeScript code chất lượng cao với strict typing, generics, utility types và advanced patterns
TypeScript Best Practices: Viết code type-safe và maintainable
TypeScript adds static typing to JavaScript, nhưng viết TypeScript “đúng cách” requires nhiều hơn là chỉ add types. Bài viết này covers best practices cho production-ready TypeScript code.
1. Strict Mode Configuration
Enable Strict Mode
// tsconfig.json
{
"compilerOptions": {
"strict": true, // Enable all strict checks
"noImplicitAny": true, // Error on implicit any
"strictNullChecks": true, // Enable strict null checks
"strictFunctionTypes": true, // Strict function type checking
"strictBindCallApply": true, // Strict bind, call, apply
"strictPropertyInitialization": true, // Strict property initialization
"noImplicitThis": true, // Error on implicit this
"alwaysStrict": true, // Parse in strict mode
"exactOptionalPropertyTypes": true, // Exact optional property types
"noUncheckedIndexedAccess": true, // Check indexed access
"noImplicitReturns": true, // Check implicit returns
"noFallthroughCasesInSwitch": true, // Check switch fallthrough
"noUncheckedSideEffectImports": true // Check side effect imports
}
}
Target và Module Configuration
{
"compilerOptions": {
"target": "ES2022", // Modern JavaScript target
"module": "ESNext", // Modern module system
"moduleResolution": "bundler", // Modern bundler resolution
"allowSyntheticDefaultImports": true, // Allow default imports
"esModuleInterop": true, // Enable interoperability
"forceConsistentCasingInFileNames": true, // Consistent file casing
"skipLibCheck": true // Skip lib checking for speed
}
}
2. Type Definitions Best Practices
Avoid any Type
// ❌ Don't use any
type UserData = any;
// ✅ Use specific types
interface UserData {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
// ✅ Use unknown when type is truly unknown
function processUnknownData(data: unknown) {
if (typeof data === 'string') {
return data.toUpperCase();
}
if (isUserData(data)) {
return processUserData(data);
}
throw new Error('Unsupported data type');
}
// ✅ Type guard functions
function isUserData(data: unknown): data is UserData {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data &&
'email' in data &&
typeof (data as any).id === 'number' &&
typeof (data as any).name === 'string' &&
typeof (data as any).email === 'string'
);
}
Use Union Types Instead of Enums
// ❌ Enum can be verbose
enum UserRole {
Admin = 'ADMIN',
User = 'USER',
Guest = 'GUEST'
}
// ✅ Union type is more flexible
type UserRole = 'admin' | 'user' | 'guest';
// ✅ Const assertion for readonly arrays
const STATUSES = ['pending', 'approved', 'rejected'] as const;
type Status = typeof STATUSES[number]; // 'pending' | 'approved' | 'rejected'
// ✅ Object with const assertion
const ROUTES = {
HOME: '/',
ABOUT: '/about',
CONTACT: '/contact'
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/about' | '/contact'
Prefer Interfaces for Object Shapes
// ✅ Interface for extensibility
interface User {
id: number;
name: string;
email: string;
}
interface AdminUser extends User {
permissions: string[];
lastLogin: Date;
}
// ✅ Type alias for complex types
type UserID = number | string;
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
// ✅ Intersection types for composition
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type SoftDeletable = {
deletedAt?: Date;
isDeleted: boolean;
};
type Document = User & Timestamped & SoftDeletable;
3. Function Types và Overloads
Function Type Definitions
// ✅ Explicit function types
type BinaryOperation = (a: number, b: number) => number;
type AsyncDataFetcher<T> = (id: string) => Promise<T>;
type EventHandler<T extends Event = Event> = (event: T) => void;
// ✅ Optional và default parameters
type GreetingFunction = (name: string, greeting?: string) => string;
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}
// ✅ Rest parameters
type LogFunction = (message: string, ...args: unknown[]) => void;
const log: LogFunction = (message, ...args) => {
console.log(`[LOG] ${message}`, ...args);
};
Function Overloads
// ✅ Function overloads for different signatures
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: boolean): boolean;
function processValue(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
}
if (typeof value === 'number') {
return value * 2;
}
return !value;
}
// ✅ Overloads for complex scenarios
class DataProcessor {
process(data: string[]): string[];
process(data: number[]): number[];
process<T>(data: T[]): T[];
process(data: unknown[]): unknown[] {
return data.map(item => {
if (typeof item === 'string') {
return item.toUpperCase();
}
if (typeof item === 'number') {
return item * 2;
}
return item;
});
}
}
4. Generics và Constraints
Basic Generics
// ✅ Generic functions
function identity<T>(value: T): T {
return value;
}
function mapArray<T, U>(array: T[], mapper: (item: T) => U): U[] {
return array.map(mapper);
}
// ✅ Generic interfaces
interface Repository<T> {
find(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(item: Omit<T, 'id'>): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
// ✅ Generic classes
class DataStore<T extends { id: string }> {
private items: Map<string, T> = new Map();
add(item: T): void {
this.items.set(item.id, item);
}
get(id: string): T | undefined {
return this.items.get(id);
}
getAll(): T[] {
return Array.from(this.items.values());
}
}
Generic Constraints
// ✅ Constrained generics
type HasId = { id: string | number };
type Timestamped = { createdAt: Date; updatedAt: Date };
function sortById<T extends HasId>(items: T[]): T[] {
return [...items].sort((a, b) => {
if (typeof a.id === 'number' && typeof b.id === 'number') {
return a.id - b.id;
}
return String(a.id).localeCompare(String(b.id));
});
}
// ✅ Multiple constraints
type Serializable = {
toJSON(): string;
fromJSON(json: string): unknown;
};
function persistData<T extends HasId & Timestamped & Serializable>(
data: T
): Promise<void> {
const json = data.toJSON();
const key = `data:${data.id}`;
return saveToStorage(key, json);
}
// ✅ Conditional types with generics
type ExtractArrayType<T> = T extends (infer U)[] ? U : never;
type ArrayItemType = ExtractArrayType<string[]>; // string
type IsPromise<T> = T extends Promise<infer U> ? U : T;
type UnwrappedType = IsPromise<Promise<string>>; // string
5. Utility Types và Type Manipulation
Built-in Utility Types
// ✅ Partial - All properties optional
type PartialUser = Partial<User>;
// ✅ Required - All properties required
type RequiredUser = Required<User>;
// ✅ Readonly - All properties readonly
type ReadonlyUser = Readonly<User>;
// ✅ Pick - Pick specific properties
type UserSummary = Pick<User, 'id' | 'name'>;
// ✅ Omit - Omit specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// ✅ Record - Create record type
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// ✅ Exclude - Exclude from union
type NonAdminRole = Exclude<UserRole, 'admin'>; // 'user' | 'guest'
// ✅ Extract - Extract from union
type AdminOnly = Extract<UserRole, 'admin'>; // 'admin'
// ✅ NonNullable - Remove null and undefined
type NonNullString = NonNullable<string | null | undefined>; // string
Custom Utility Types
// ✅ Deep partial type
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// ✅ Nullable type
type Nullable<T> = T | null;
// ✅ Optional (undefined) type
type Optional<T> = T | undefined;
// ✅ Maybe type (null or undefined)
type Maybe<T> = T | null | undefined;
// ✅ Result type for error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// ✅ Async result type
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
// ✅ Branded types for type safety
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
function createUserId(id: string): UserId {
return id as UserId;
}
function getUserById(id: UserId): User {
// Implementation
}
// Type-safe! Can't accidentally pass ProductId
const userId = createUserId('user-123');
const user = getUserById(userId); // ✅ OK
6. Advanced Type Patterns
Template Literal Types
// ✅ Event name types
type EventName =
| `user:${'created' | 'updated' | 'deleted'}`
| `order:${'placed' | 'shipped' | 'delivered' | 'cancelled'}`;
// ✅ CSS property types
type CSSProperty =
| `margin${'' | '-top' | '-right' | '-bottom' | '-left'}`
| `padding${'' | '-top' | '-right' | '-bottom' | '-left'}`;
// ✅ API endpoint types
type APIEndpoint = `/api/${'users' | 'products' | 'orders'}/${string}`;
// ✅ String manipulation types
type Capitalize<S extends string> =
S extends `${infer First}${infer Rest}`
? `${Uppercase<First>}${Rest}`
: S;
type CamelCase<S extends string> =
S extends `${infer First}_${infer Second}${infer Rest}`
? `${First}${Uppercase<Second>}${CamelCase<Rest>}`
: S;
Discriminated Unions
// ✅ Action types with discriminated unions
interface BaseAction {
type: string;
}
interface LoginAction extends BaseAction {
type: 'LOGIN';
payload: {
username: string;
password: string;
};
}
interface LogoutAction extends BaseAction {
type: 'LOGOUT';
}
interface UpdateProfileAction extends BaseAction {
type: 'UPDATE_PROFILE';
payload: {
name: string;
email: string;
};
}
type AppAction =
| LoginAction
| LogoutAction
| UpdateProfileAction;
// ✅ Type-safe reducer
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'LOGIN':
return {
...state,
user: action.payload,
isAuthenticated: true
};
case 'LOGOUT':
return {
...state,
user: null,
isAuthenticated: false
};
case 'UPDATE_PROFILE':
return {
...state,
user: state.user ? {
...state.user,
...action.payload
} : null
};
default:
return state;
}
}
Recursive Types
// ✅ JSON type
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// ✅ Tree structure
type TreeNode<T> = {
value: T;
children?: TreeNode<T>[];
};
// ✅ Nested object type
type NestedObject<T> = {
[K in keyof T]: T[K] extends object ? NestedObject<T[K]> : T[K];
};
// ✅ Deep readonly type
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
7. Error Handling và Validation
Type-Safe Error Handling
// ✅ Result type with discriminated unions
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// ✅ Async result type
interface ValidationError {
field: string;
message: string;
value: unknown;
}
type ValidationResult<T> =
| { isValid: true; data: T }
| { isValid: false; errors: ValidationError[] };
// ✅ Validation functions
function validateEmail(email: unknown): Result<string, ValidationError> {
if (typeof email !== 'string') {
return {
success: false,
error: {
field: 'email',
message: 'Email must be a string',
value: email
}
};
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return {
success: false,
error: {
field: 'email',
message: 'Invalid email format',
value: email
}
};
}
return { success: true, data: email };
}
// ✅ Validation pipeline
function validateUser(data: unknown): ValidationResult<User> {
const errors: ValidationError[] = [];
const emailResult = validateEmail((data as any).email);
if (!emailResult.success) {
errors.push(emailResult.error);
}
const nameResult = validateString((data as any).name, 'name', 2, 50);
if (!nameResult.success) {
errors.push(nameResult.error);
}
if (errors.length > 0) {
return { isValid: false, errors };
}
return {
isValid: true,
data: {
id: generateId(),
name: nameResult.data,
email: emailResult.data,
createdAt: new Date()
}
};
}
8. Performance Considerations
Type-only Imports
// ✅ Type-only imports (no runtime impact)
import type { User, UserRole } from './types';
import { createUser } from './user-service';
// ✅ Re-export types only
export type { User, UserRole } from './types';
export { createUser, updateUser } from './user-service';
Const Assertions
// ✅ Use const assertions for compile-time constants
const ROUTES = {
HOME: '/',
ABOUT: '/about',
CONTACT: '/contact'
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/about' | '/contact'
// ✅ Const enums for zero runtime cost
const enum Status {
Pending = 'PENDING',
Approved = 'APPROVED',
Rejected = 'REJECTED'
}
9. Testing với TypeScript
Type-Safe Test Utilities
// ✅ Generic test helpers
function createMockUser(overrides?: Partial<User>): User {
return {
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'user',
createdAt: new Date(),
...overrides
};
}
// ✅ Type-safe assertions
function assertUser(user: unknown): asserts user is User {
if (!isUser(user)) {
throw new Error('Value is not a valid User');
}
}
// ✅ Test fixtures with proper types
interface TestFixtures {
users: User[];
posts: Post[];
comments: Comment[];
}
function createTestFixtures(): TestFixtures {
const users: User[] = [
createMockUser({ id: 1, name: 'Alice' }),
createMockUser({ id: 2, name: 'Bob', role: 'admin' })
];
const posts: Post[] = [
{ id: 1, title: 'First Post', authorId: 1, content: 'Hello World' },
{ id: 2, title: 'Second Post', authorId: 2, content: 'TypeScript Tips' }
];
return { users, posts, comments: [] };
}
10. Common Anti-Patterns to Avoid
Type Assertion Abuse
// ❌ Don't abuse type assertions
const user = getData() as User;
const element = document.getElementById('app') as HTMLDivElement;
// ✅ Use type guards and proper typing
function getUserData(): User | null {
const data = getData();
return isUser(data) ? data : null;
}
function getElement<T extends HTMLElement>(id: string): T | null {
return document.getElementById(id) as T | null;
}
Over-Engineering Types
// ❌ Don't over-complicate simple types
type UserID = Brand<string, 'UserID'> &
{ readonly __userIdBrand: unique symbol } &
{ length: 36 };
// ✅ Keep it simple but type-safe
type UserID = string; // With validation function
function validateUserId(id: string): Result<UserID, ValidationError> {
if (id.length !== 36) {
return {
success: false,
error: {
field: 'id',
message: 'User ID must be 36 characters',
value: id
}
};
}
return { success: true, data: id };
}
Kết luận
TypeScript best practices giúp bạn:
- Enable strict mode - Catch bugs early
- Use proper types - Avoid
anyvà implicit types - Leverage generics - Write reusable, type-safe code
- Use utility types - Don’t reinvent the wheel
- Handle errors properly - Type-safe error handling
- Test thoroughly - Type-safe test utilities
- Avoid anti-patterns - Keep it simple and maintainable
💡 Pro tip: TypeScript learning là continuous journey. Start với strict mode, practice với generics, và gradually explore advanced patterns. Quality over complexity luôn là key!
Bài viết này là phần đầu trong series về TypeScript advanced patterns. Trong các bài tiếp theo, chúng ta sẽ explore template literal types, conditional types, và design patterns with TypeScript.