typescript-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

When to use

适用场景

Use this skill when working with TypeScript code. AI agents frequently generate outdated patterns - using
any
instead of
unknown
, type assertions instead of
satisfies
, optional fields instead of discriminated unions, and missing strict mode options. This skill enforces modern TypeScript 5.x patterns.
在编写TypeScript代码时使用本技能。AI Agent经常生成过时的模式——用
any
而非
unknown
,用类型断言而非
satisfies
,用可选字段而非区分联合类型,还会遗漏严格模式配置。本技能可强制采用TypeScript 5.x的现代模式。

Critical Rules

核心规则

1. Enable Strict Mode with All Checks

1. 启用全检查的严格模式

Wrong (agents do this):
json
{
  "compilerOptions": {
    "strict": false,
    "target": "ES2020"
  }
}
Correct:
json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "target": "ES2022"
  }
}
Why: Strict mode catches entire categories of bugs.
noUncheckedIndexedAccess
prevents unsafe array/object access. Agents often skip these for "convenience."
错误写法(Agent常这么写):
json
{
  "compilerOptions": {
    "strict": false,
    "target": "ES2020"
  }
}
正确写法:
json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "target": "ES2022"
  }
}
原因: 严格模式可以排查整类bug。
noUncheckedIndexedAccess
能防止不安全的数组/对象访问。Agent常常为了“省事”跳过这些配置。

2. Use satisfies Instead of Type Assertions

2. 使用satisfies而非类型断言

Wrong (agents do this):
typescript
const config = {
  port: 3000,
  host: "localhost",
} as Config;

config.port.toFixed(); // No error even if port could be string
Correct:
typescript
const config = {
  port: 3000,
  host: "localhost",
} satisfies Config;

config.port.toFixed(); // TypeScript knows port is number
Why:
satisfies
validates the type without widening it.
as
silences the compiler and can hide bugs. Use
satisfies
for validation,
as
only when you genuinely know more than the compiler.
错误写法(Agent常这么写):
typescript
const config = {
  port: 3000,
  host: "localhost",
} as Config;

config.port.toFixed(); // 即使port可能是string也不会报错
正确写法:
typescript
const config = {
  port: 3000,
  host: "localhost",
} satisfies Config;

config.port.toFixed(); // TypeScript知道port是number类型
原因:
satisfies
在验证类型的同时不会拓宽类型。
as
会让编译器静默,可能隐藏bug。用
satisfies
做类型验证,只有当你确实比编译器更了解类型时才使用
as

3. Use Discriminated Unions Over Optional Fields

3. 使用区分联合类型而非可选字段

Wrong (agents do this):
typescript
interface ApiResponse {
  data?: User;
  error?: string;
  loading?: boolean;
}
Correct:
typescript
type ApiResponse =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };
Why: Optional fields allow impossible states (data AND error both present). Discriminated unions make each state explicit and exhaustively checkable.
错误写法(Agent常这么写):
typescript
interface ApiResponse {
  data?: User;
  error?: string;
  loading?: boolean;
}
正确写法:
typescript
type ApiResponse =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };
原因: 可选字段会允许不可能的状态(同时存在data和error)。区分联合类型能让每个状态都显式化,并且可以被穷尽检查。

4. Use const Assertions for Literal Types

4. 对字面量类型使用const断言

Wrong (agents do this):
typescript
const ROUTES = {
  home: "/",
  about: "/about",
  contact: "/contact",
};
// Type: { home: string; about: string; contact: string }
Correct:
typescript
const ROUTES = {
  home: "/",
  about: "/about",
  contact: "/contact",
} as const;
// Type: { readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }
Why: Without
as const
, TypeScript widens literal types to
string
. With it, you get exact literal types and readonly properties.
错误写法(Agent常这么写):
typescript
const ROUTES = {
  home: "/",
  about: "/about",
  contact: "/contact",
};
// 类型:{ home: string; about: string; contact: string }
正确写法:
typescript
const ROUTES = {
  home: "/",
  about: "/about",
  contact: "/contact",
} as const;
// 类型:{ readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }
原因: 没有
as const
时,TypeScript会把字面量类型拓宽为
string
。使用它之后,你会得到精确的字面量类型和只读属性。

5. Use unknown Instead of any

5. 使用unknown而非any

Wrong (agents do this):
typescript
function parseJson(text: string): any {
  return JSON.parse(text);
}

const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // No error - runtime crash
Correct:
typescript
function parseJson(text: string): unknown {
  return JSON.parse(text);
}

const data = parseJson('{"name": "test"}');
if (isUser(data)) {
  data.name; // Safe - type narrowed
}
Why:
any
disables all type checking.
unknown
forces you to narrow the type before using it, catching bugs at compile time.
错误写法(Agent常这么写):
typescript
function parseJson(text: string): any {
  return JSON.parse(text);
}

const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // 无报错——运行时会崩溃
正确写法:
typescript
function parseJson(text: string): unknown {
  return JSON.parse(text);
}

const data = parseJson('{"name": "test"}');
if (isUser(data)) {
  data.name; // 安全——类型已收窄
}
原因:
any
会禁用所有类型检查。
unknown
会强制你在使用前收窄类型,在编译时就能发现bug。

6. Use Template Literal Types for String Patterns

6. 对字符串模式使用模板字面量类型

Wrong (agents do this):
typescript
function getLocaleMessage(id: string): string { ... }
Correct:
typescript
type Locale = 'en' | 'ja' | 'pt';
type MessageKey = 'welcome' | 'goodbye';
type LocaleMessageId = `${Locale}_${MessageKey}`;

function getLocaleMessage(id: LocaleMessageId): string { ... }
Why: Template literal types create precise string patterns from unions. The compiler catches typos and invalid combinations at build time.
错误写法(Agent常这么写):
typescript
function getLocaleMessage(id: string): string { ... }
正确写法:
typescript
type Locale = 'en' | 'ja' | 'pt';
type MessageKey = 'welcome' | 'goodbye';
type LocaleMessageId = `${Locale}_${MessageKey}`;

function getLocaleMessage(id: LocaleMessageId): string { ... }
原因: 模板字面量类型可以从联合类型创建精确的字符串模式。编译器会在构建时捕获拼写错误和无效组合。

7. Use NoInfer to Prevent Unwanted Inference

7. 使用NoInfer避免不必要的类型推断

Wrong (agents do this):
typescript
function createLight<C extends string>(colors: C[], defaultColor?: C) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // No error - purple widens C
Correct:
typescript
function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // Error - 'purple' not in C
Why:
NoInfer<T>
(TypeScript 5.4+) prevents a parameter from influencing type inference, ensuring stricter checks.
错误写法(Agent常这么写):
typescript
function createLight<C extends string>(colors: C[], defaultColor?: C) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // 无报错——purple拓宽了C的类型
正确写法:
typescript
function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // 报错——'purple'不在C中
原因:
NoInfer<T>
(TypeScript 5.4+)可以防止参数影响类型推断,确保更严格的检查。

8. Use Branded Types for Type-Safe IDs

8. 使用品牌类型实现类型安全的ID

Wrong (agents do this):
typescript
function getUser(id: string): User { ... }
function getOrder(id: string): Order { ... }

const userId = getUserId();
getOrder(userId); // No error - but wrong!
Correct:
typescript
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

function getUser(id: UserId): User { ... }
function getOrder(id: OrderId): Order { ... }

const userId = getUserId();
getOrder(userId); // Error - UserId is not OrderId
Why: Branded types prevent accidentally passing one ID type where another is expected. The brand exists only at compile time - zero runtime cost.
错误写法(Agent常这么写):
typescript
function getUser(id: string): User { ... }
function getOrder(id: string): Order { ... }

const userId = getUserId();
getOrder(userId); // 无报错——但这是错误的!
正确写法:
typescript
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

function getUser(id: UserId): User { ... }
function getOrder(id: OrderId): Order { ... }

const userId = getUserId();
getOrder(userId); // 报错——UserId不是OrderId
原因: 品牌类型可以防止意外地将一种ID类型传入需要另一种ID类型的地方。品牌仅在编译时存在——运行时无额外开销。

9. Use Exhaustive Switch with never

9. 结合never使用穷尽switch语句

Wrong (agents do this):
typescript
function handleStatus(status: "active" | "inactive" | "pending") {
  switch (status) {
    case "active":
      return "Active";
    case "inactive":
      return "Inactive";
    // 'pending' silently falls through
  }
}
Correct:
typescript
function handleStatus(status: "active" | "inactive" | "pending") {
  switch (status) {
    case "active":
      return "Active";
    case "inactive":
      return "Inactive";
    case "pending":
      return "Pending";
    default: {
      const _exhaustive: never = status;
      throw new Error(`Unhandled status: ${_exhaustive}`);
    }
  }
}
Why: The
never
check ensures every union member is handled. When a new status is added, the compiler flags the missing case.
错误写法(Agent常这么写):
typescript
function handleStatus(status: "active" | "inactive" | "pending") {
  switch (status) {
    case "active":
      return "Active";
    case "inactive":
      return "Inactive";
    // 'pending'被静默忽略
  }
}
正确写法:
typescript
function handleStatus(status: "active" | "inactive" | "pending") {
  switch (status) {
    case "active":
      return "Active";
    case "inactive":
      return "Inactive";
    case "pending":
      return "Pending";
    default: {
      const _exhaustive: never = status;
      throw new Error(`未处理的状态:${_exhaustive}`);
    }
  }
}
原因:
never
检查确保联合类型的每个成员都被处理。当添加新状态时,编译器会标记缺失的分支。

10. Use Type Predicates Over Type Assertions

10. 使用类型谓词而非类型断言

Wrong (agents do this):
typescript
function processItem(item: unknown) {
  const user = item as User;
  console.log(user.name);
}
Correct:
typescript
function isUser(item: unknown): item is User {
  return typeof item === "object" && item !== null && "name" in item && "email" in item;
}

function processItem(item: unknown) {
  if (isUser(item)) {
    console.log(item.name); // Safe - narrowed to User
  }
}
Why: Type predicates (
item is User
) narrow types safely with runtime checks. Type assertions (
as User
) bypass the compiler and can hide bugs.
错误写法(Agent常这么写):
typescript
function processItem(item: unknown) {
  const user = item as User;
  console.log(user.name);
}
正确写法:
typescript
function isUser(item: unknown): item is User {
  return typeof item === "object" && item !== null && "name" in item && "email" in item;
}

function processItem(item: unknown) {
  if (isUser(item)) {
    console.log(item.name); // 安全——类型已收窄为User
  }
}
原因: 类型谓词(
item is User
)通过运行时检查安全地收窄类型。类型断言(
as User
)会绕过编译器,可能隐藏bug。

11. Use import type for Type-Only Imports

11. 对仅类型导入使用import type

Wrong (agents do this):
typescript
import { User, UserService } from "./user";
// User is only used as a type, but gets included in the bundle
Correct:
typescript
import type { User } from "./user";
import { UserService } from "./user";
Why:
import type
is erased at compile time, reducing bundle size. It also makes the intent clear - this import is for types only.
错误写法(Agent常这么写):
typescript
import { User, UserService } from "./user";
// User仅作为类型使用,但会被打包进产物
正确写法:
typescript
import type { User } from "./user";
import { UserService } from "./user";
原因:
import type
在编译时会被移除,减小包体积。同时也能明确表达意图——这个导入仅用于类型。

12. Use Record Over Index Signatures

12. 使用Record而非索引签名

Wrong (agents do this):
typescript
interface Config {
  [key: string]: string;
}
Correct:
typescript
type Config = Record<string, string>;

// Or better - use a specific union for keys:
type Config = Record<"host" | "port" | "env", string>;
Why:
Record<K, V>
is more readable and composable than index signatures. When possible, use a union for keys to get exhaustive checking.
错误写法(Agent常这么写):
typescript
interface Config {
  [key: string]: string;
}
正确写法:
typescript
type Config = Record<string, string>;

// 更好的方式——对键使用特定的联合类型:
type Config = Record<"host" | "port" | "env", string>;
原因:
Record<K, V>
比索引签名更易读且更具组合性。如果可能,对键使用联合类型以获得穷尽检查。

13. Use using for Resource Management

13. 使用using进行资源管理

Wrong (agents do this):
typescript
const file = openFile("data.txt");
try {
  processFile(file);
} finally {
  file.close();
}
Correct:
typescript
using file = openFile("data.txt");
processFile(file);
// file.close() called automatically via Symbol.dispose
Why: The
using
keyword (TypeScript 5.2+) provides deterministic resource cleanup via the Disposable protocol, similar to Python's
with
or C#'s
using
.
错误写法(Agent常这么写):
typescript
const file = openFile("data.txt");
try {
  processFile(file);
} finally {
  file.close();
}
正确写法:
typescript
using file = openFile("data.txt");
processFile(file);
// file.close()会通过Symbol.dispose自动调用
原因:
using
关键字(TypeScript 5.2+)通过Disposable协议提供确定性的资源清理,类似于Python的
with
或C#的
using

Patterns

推荐模式

  • Enable
    strict: true
    and
    noUncheckedIndexedAccess: true
    in every project
  • Use
    satisfies
    for type validation without widening
  • Use discriminated unions with a
    type
    or
    kind
    field for state modeling
  • Use
    as const
    for configuration objects and route maps
  • Use branded types for domain-specific IDs
  • Use
    import type
    for all type-only imports
  • Use exhaustive
    switch
    with
    never
    default for union handling
  • 在所有项目中启用
    strict: true
    noUncheckedIndexedAccess: true
  • 使用
    satisfies
    进行类型验证且不拓宽类型
  • 对状态建模时,使用带有
    type
    kind
    字段的区分联合类型
  • 对配置对象和路由映射使用
    as const
  • 对领域特定ID使用品牌类型
  • 对所有仅类型导入使用
    import type
  • 处理联合类型时,结合
    never
    默认分支使用穷尽
    switch
    语句

Anti-Patterns

反模式

  • NEVER use
    any
    - use
    unknown
    and narrow with type guards
  • NEVER use
    as
    for type assertions unless you genuinely know more than the compiler
  • NEVER use optional fields to model mutually exclusive states - use discriminated unions
  • NEVER use
    // @ts-ignore
    or
    // @ts-expect-error
    without a comment explaining why
  • NEVER use
    enum
    - use
    as const
    objects or union types instead
  • NEVER use
    Function
    type - use specific function signatures
  • NEVER disable strict mode for convenience
  • 绝对不要使用
    any
    ——用
    unknown
    并结合类型守卫收窄类型
  • 绝对不要用
    as
    做类型断言,除非你确实比编译器更了解类型
  • 绝对不要用可选字段建模互斥状态——使用区分联合类型
  • 绝对不要在没有注释说明原因的情况下使用
    // @ts-ignore
    // @ts-expect-error
  • 绝对不要使用
    enum
    ——改用
    as const
    对象或联合类型
  • 绝对不要使用
    Function
    类型——改用特定的函数签名
  • 绝对不要为了省事而禁用严格模式