TypeScript: TypeScript Best Practices
Last updated: 2026-08-26
Mastering grammar is just the beginning—to write good TypeScript, you need to follow a set of best practices. This lesson summarizes the most valuable insights and principles from real-world projects.
1. Naming Conventions
(1) Type Naming
| Category | Specification | Example |
|---|---|---|
| Interface | PascalCase | UserService, ApiResponse |
| Type Alias | PascalCase | Status, EventHandler |
| Generic Parameter | Single Letter or PascalCase | T, K, TItem |
| Enumeration | PascalCase (values may be uppercase) | HttpStatus, COLOR_RED |
| Enumeration Members | PascalCase | HttpStatus.Ok |
(2) File Naming
| Type | Specification | Example |
|---|---|---|
| Regular Module | camelCase | userService.ts |
| File Type | PascalCase | UserController.ts |
| Declaration File | Same Name as Module | myutils.d.ts |
| Test File | Module Name.test | userService.test.ts |
(3) Export Specifications
TYPESCRIPT
// ✅ Recommendations——Prioritize naming and exporting
export function addUser(user: User): void { }
export class UserController { }
export type Status = "active" | "inactive";
// ⚠️ Use Default Export with Caution——It's easy to make mistakes when renaming files
export default class UserController { }
// ✅ Library/Framework recommended — both options are available
export class UserController { }
export default UserController;
2. Type Design Principles
(1) Principle 1: Precision over breadth
TYPESCRIPT
// ❌ Broad——Type information lost
function process(value: any): any { }
// ❌ Slightly better, but still too broad
function process(value: string | number): string | number { }
// ✅ Accurate——Generics Preserve Type Information
function process<T extends string | number>(value: T): T { }
(2) Principle 2: Calculating is better than writing by hand
TYPESCRIPT
// ❌ Manually maintain two locations——Prone to desynchronization
interface User { id: number; name: string; email: string; }
type UserKeys = "id" | "name" | "email";
// ✅ Inference Based on Source Type——Automatic Synchronization
type UserKeys2 = keyof User; // "id" | "name" | "email"
type UserValues = User[keyof User]; // number | string
(3) Principle 3: Composition Over Inheritance
TYPESCRIPT
// ❌ Deep Inheritance——The Problem with Fragile Base Classes
class BaseEntity { id: number; }
class TimestampedEntity extends BaseEntity { createdAt: Date; }
class FullEntity extends TimestampedEntity { createdBy: string; }
// ✅ Type Combinations——Flexible and decoupled
type WithId = { id: number };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithAudit = { createdBy: string; updatedBy: string };
type FullEntity2 = WithId & WithTimestamps & WithAudit;
type SimpleEntity = WithId; // Customizable Combinations
▶ Example: Refactoring "any" to be type-safe
TYPESCRIPT
// ❌ Before the refactoring——everywhere any,Zero Type Safety
function processRequest(req: any): any {
let user = req.body.user; // any
let result = validate(user); // any
return { status: 200, data: result };
}
// ✅ After the refactoring——Type safety at every step
interface User { id: number; name: string; email: string; }
interface Request2 { body: { user: User } }
interface ValidationResult { valid: boolean; errors?: string[] }
interface Response2<T> { status: number; data: T }
function processRequest2(req: Request2): Response2<ValidationResult> {
let user: User = req.body.user; // ✅ User Type
let result: ValidationResult = validate2(user); // ✅ ValidationResult
return { status: 200, data: result };
}
function validate2(user: User): ValidationResult {
if (!user.email.includes("@")) {
return { valid: false, errors: ["Invalid email address"] };
}
return { valid: true };
}
3. Alternatives to any
(1) unknown as an Alternative to any
TYPESCRIPT
// ❌ any——Turn off type checking
function process(value: any) {
return value.toUpperCase(); // Do not check,May crash during runtime
}
// ✅ unknown——You must narrow it first before you can use it.
function process2(value: unknown) {
if (typeof value === "string") {
return value.toUpperCase(); // ✅ Safe Use After Narrowing
}
throw new Error("Expectations string Type");
}
(2) Generics as a Replacement for any
TYPESCRIPT
// ❌ any——Missing Type Information
function first(arr: any[]): any {
return arr[0];
}
// ✅ Generics——Preserve Type Information
function first2<T>(arr: T[]): T {
return arr[0];
}
(3) Using Union Types Instead of any
TYPESCRIPT
// ❌ any
let value: any;
// ✅ Composite Types——Clearly list the possible types
let value2: string | number | boolean;
(4) Replacing any objects with index signatures
TYPESCRIPT
// ❌ any Object
let config: any = { host: "localhost" };
// ✅ Index Signature
let config2: Record<string, string | number> = { host: "localhost" };
4. DRY Principle (Don't Repeat Yourself)
(1) Use keyof, typeof, and utility types to avoid duplication
TYPESCRIPT
const THEMES = {
light: { bg: "#fff", text: "#333" },
dark: { bg: "#1a1a1a", text: "#e0e0e0" }
} as const;
// Type Inference from Values——No duplicate definitions
type ThemeName = keyof typeof THEMES; // "light" | "dark"
type ThemeColors = typeof THEMES["light"]; // { readonly bg: "..."; readonly text: "..." }
function getTheme(name: ThemeName): ThemeColors {
return THEMES[name];
}
(2) Batch transformations using mapping types
TYPESCRIPT
interface ApiUser {
id: number;
name: string;
email: string;
role: string;
}
// No handwriting required——Derivation Using Tool Types
type CreateUserDTO = Omit<ApiUser, "id">;
type UpdateUserDTO = Partial<Omit<ApiUser, "id">>;
type UserSummary = Pick<ApiUser, "id" | "name">;
type UserResponse = Readonly<ApiUser>;
5. Type Narrowing Strategies
(1) Narrow as early as possible
TYPESCRIPT
// ❌ The gap is narrowing——Check each location before use
function process(value: string | number) {
console.log(value.toString()); // Only shared methods can be used
if (typeof value === "string") {
console.log(value.toUpperCase());
}
// Further checks will be needed later....
}
// ✅ Narrow as soon as possible——Use directly within the branch
function process2(value: string | number) {
if (typeof value === "string") {
// The entire branch is string
console.log(value.toUpperCase());
console.log(value.trim());
return;
}
// This must be number
console.log(value.toFixed(2));
}
(2) Reusing narrowing logic in custom type guards
TYPESCRIPT
// The Complex Logic Behind Narrowing——Extract as a type guard
function isValidUser(obj: any): obj is User {
return obj
&& typeof obj.id === "number"
&& typeof obj.name === "string"
&& typeof obj.email === "string";
}
// Reuse in Multiple Places
function processUser(data: unknown) {
if (isValidUser(data)) {
console.log(data.name); // ✅ Type Safety
}
}
6. Guidelines for Team Collaboration
(1) Unified tsconfig Configuration
JSON
{
"compilerOptions": {
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
The same rules are automatically applied to all team members' IDEs—no manual configuration is required.
(2) ESLint TypeScript Rules
JSON
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/explicit-function-return-type": "warn"
}
}
(3) Code Review Checklist
- [ ] No
anytype (unless a comment explains the reason) - [ ] Function parameters and return values have type annotations
- [ ] Checks for values that may be null or undefined
- [ ]
@ts-ignorewas not used (replaced with@ts-expect-error) - [ ] The public API includes JSDoc comments
- [ ] The type import uses
import type
▶ Example: Strict Mode Catching Real Bugs
TYPESCRIPT
// strict: true catches null/undefined bugs at compile time
interface SearchResult { items: string[]; total: number; }
function search(query: string): SearchResult | null {
if (!query.trim()) return null;
return { items: [`Result for ${query}`], total: 1 };
}
// Without strict: compiles but crashes at runtime
// let result = search("");
// console.log(result.items.length); // TypeError at runtime
// With strict: compiler forces null check
let result = search("");
if (result) {
console.log(result.items.length); // ✅ safe after narrowing
} else {
console.log("No query provided");
}
Output:
TEXT
📖 Display only
No query provided
▶ Example: Eliminating any with Generics
TYPESCRIPT
// ❌ Before: loose types with any
function getProp(obj: any, key: string): any {
return obj[key];
}
let user: any = { name: "Alice", age: 30 };
let name2 = getProp(user, "name"); // any — no autocomplete
// ✅ After: generics preserve type information
function getProp2<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let user2 = { name: "Alice", age: 30 } as const;
let name3 = getProp2(user2, "name"); // "Alice" — exact literal type
let age = getProp2(user2, "age"); // 30 — exact literal type
// getProp2(user2, "email"); // ❌ not assignable to keyof
Output:
TEXT
📖 Display only
Compile error — Argument of type "email" is not assignable to keyof
❓ FAQ
Q Should the use of
any be completely prohibited in a project?A That’s not realistic. Set ESLint to
no-explicit-any: warn rather than error—this allows a small number of any usages but prompts a review. any is reasonable in scenarios such as untyped third-party libraries, complex type conversions, and rapid prototyping. The key is to include comments explaining the reason and have a plan for replacement.Q Should type definitions be placed in a separate file or within the file they’re used in?
A Public/shared types should be placed in the
types/ or models/ directory; types used only within a single file should be defined directly within that file. Rule: If a type is referenced by three or more files, extract it to a public type file; otherwise, define it within the file where it’s used.Q Should I use ESLint for a TypeScript project?
A Yes. tsc handles type checking, while ESLint handles code quality checks—the two complement each other rather than replace one another. The
@typescript-eslint plugin provides TypeScript-specific rules (such as no-explicit-any and consistent-type-imports), making it a standard choice for TypeScript projects.Q What should I do if there are too many generic type parameters?
A If there are more than three type parameters, consider the following: (1) Replace multiple generics with object parameters; (2) Extract some types into separate interfaces; (3) Use generic default values to reduce the number of parameters that must be specified. Too many type parameters usually indicate an inappropriate level of abstraction.
📖 Summary
- Naming conventions: Use PascalCase for interfaces, types, and classes; use a single letter or PascalCase for generics
- Type Design: Precision over generality, derivation over hand-coding, composition over inheritance
- Alternatives to
any:unknown(safety fallback), generics (type preservation), union types (explicit enumeration) - DRY Type: Derived from the source using
keyof,typeof, or utility types; no duplicate definitions - Type narrowing: Narrow as early as possible; reuse type guards
- Team Guidelines: Standardized tsconfig, ESLint rules, and code review checklists
📝 Exercises
- Basic Exercise (Difficulty ⭐): Find an example of the
anytype that you’ve written or seen, and replace it withunknownor generics. Make sure the functionality remains the same after the replacement and that the code is more type-safe. - Advanced Problem (Difficulty ⭐⭐): Refactor a set of type definitions using the DRY principle—given the
ApiProductinterface, use utility types to derive the four typesCreateProduct,UpdateProduct,ProductSummary, andProductResponse, without manually writing any duplicate properties. - Challenge (Difficulty: ⭐⭐⭐): Write a TypeScript coding standards document for your team—including naming conventions, alternatives to
any, type import rules, recommendedtsconfigsettings, and recommended ESLint rules. Provide the rationale for each rule, along with examples and counterexamples.