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 };
}
▶ Try it Yourself

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


▶ 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");
}
▶ Try it Yourself

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
▶ Try it Yourself

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

📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Find an example of the any type that you’ve written or seen, and replace it with unknown or generics. Make sure the functionality remains the same after the replacement and that the code is more type-safe.
  2. Advanced Problem (Difficulty ⭐⭐): Refactor a set of type definitions using the DRY principle—given the ApiProduct interface, use utility types to derive the four types CreateProduct, UpdateProduct, ProductSummary, and ProductResponse, without manually writing any duplicate properties.
  3. Challenge (Difficulty: ⭐⭐⭐): Write a TypeScript coding standards document for your team—including naming conventions, alternatives to any, type import rules, recommended tsconfig settings, and recommended ESLint rules. Provide the rationale for each rule, along with examples and counterexamples.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏