mcpField
T
typescript-best-practices

typescript-best-practices

github.com/ofershap开发9325 次安装37 周前更新
代码仓库

关于 typescript-best-practices

面向处理 TypeScript 代码的 AI Agent,介绍 TypeScript 5.x 的现代模式,帮助增强类型安全并在编译时发现错误。

主要功能

  1. 01启用严格编译选项
  2. 02使用 unknown 并收窄类型
  3. 03优先用 satisfies 替代断言
  4. 04用可辨识联合建模状态
  5. 05检查 switch 分支完整性
  6. 06用品牌类型保护 ID

适用场景

  1. 01检查 TypeScript 代码中的不安全模式
  2. 02设计类型化配置和 API 状态
  3. 03配置严格的 TypeScript 项目

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ofershap/typescript-best-practices --skill typescript-best-practices

安装后在 Claude Code 中输入/typescript-best-practices确认已加载。

详细介绍

适用场景

在处理 TypeScript 代码时使用此技能。AI Agent 经常生成过时的模式——使用 any 而不是 unknown,使用类型断言而不是 satisfies,使用可选字段而不是可辨识联合,以及遗漏严格模式选项。此技能可强制采用现代 TypeScript 5.x 模式。

关键规则

1. 启用包含所有检查的严格模式

错误(Agent 会这样做):

{
  "compilerOptions": {
    "strict": false,
    "target": "ES2020"
  }
}

正确:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "target": "ES2022"
  }
}

原因: 严格模式可以捕获整类错误。noUncheckedIndexedAccess 可防止不安全的数组/对象访问。Agent 经常为了“方便”而跳过这些设置。

2. 使用 satisfies 代替类型断言

错误(Agent 会这样做):

const config = {
  port: 3000,
  host: "localhost",
} as Config;

config.port.toFixed(); // No error even if port could be string

正确:

const config = {
  port: 3000,
  host: "localhost",
} satisfies Config;

config.port.toFixed(); // TypeScript knows port is number

原因: satisfies 会验证类型,但不会扩大类型。as 会绕过编译器检查,可能隐藏错误。使用 satisfies 进行验证,只有在确实比编译器更了解情况时才使用 as。

3. 使用可辨识联合,而不是可选字段

错误(Agent 会这样做):

interface ApiResponse {
  data?: User;
  error?: string;
  loading?: boolean;
}

正确:

type ApiResponse =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };

原因: 可选字段允许出现不可能的状态(数据和错误同时存在)。可辨识联合能明确表示每种状态,并支持穷尽检查。

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

错误(Agent 会这样做):

const ROUTES = {
  home: "/",
  about: "/about",
  contact: "/contact",
};
// Type: { home: string; about: string; contact: string }

正确:

const ROUTES = {
  home: "/",
  about: "/about",
  contact: "/contact",
} as const;
// Type: { readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }

原因: 不使用 as const 时,TypeScript 会将字面量类型扩大为 string。使用它后,便能获得精确的字面量类型和只读属性。

5. 使用 unknown 代替 any

错误(Agent 会这样做):

function parseJson(text: string): any {
  return JSON.parse(text);
}

const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // No error - runtime crash

正确:

function parseJson(text: string): unknown {
  return JSON.parse(text);
}

const data = parseJson('{"name": "test"}');
if (isUser(data)) {
  data.name; // Safe - type narrowed
}

原因: any 会禁用所有类型检查。unknown 会强制你在使用类型之前先将其收窄,从而在编译时捕获错误。

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

错误(Agent 会这样做):

function getLocaleMessage(id: string): string { ... }

正确:

type Locale = 'en' | 'ja' | 'pt';
type MessageKey = 'welcome' | 'goodbye';
type LocaleMessageId = `${Locale}_${MessageKey}`;

function getLocaleMessage(id: LocaleMessageId): string { ... }

原因: 模板字面量类型可根据联合类型创建精确的字符串模式。编译器会在构建时捕获拼写错误和无效组合。

7. 使用 NoInfer 避免不需要的推断

错误(Agent 会这样做):

function createLight<C extends string>(colors: C[], defaultColor?: C) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // No error - purple widens C

正确:

function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // Error - 'purple' not in C

原因: NoInfer<T>(TypeScript 5.4+)可防止参数影响类型推断,从而确保进行更严格的检查。

8. 使用品牌类型确保 ID 的类型安全

错误(Agent 会这样做):

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

const userId = getUserId();
getOrder(userId); // No error - but wrong!

正确:

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

原因: 品牌类型可防止误将一种 ID 类型传入需要另一种 ID 类型的位置。品牌仅在编译时存在——不会产生任何运行时开销。

9. 使用 never 进行穷尽式 switch 检查

错误(Agent 会这样做):

function handleStatus(status: "active" | "inactive" | "pending") {
  switch (status) {
    case "active":
      return "Active";
    case "inactive":
      return "Inactive";
    // 'pending' silently falls through
  }
}

正确:

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}`);
    }
  }
}

原因: never 检查可确保处理联合类型中的每个成员。添加新状态时,编译器会标记缺失的分支。

10. 使用类型谓词代替类型断言

错误(Agent 会这样做):

function processItem(item: unknown) {
  const user = item as User;
  console.log(user.name);
}

正确:

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
  }
}

原因: 类型谓词(item is User)通过运行时检查安全地收窄类型。类型断言(as User)会绕过编译器,可能隐藏错误。

11. 使用 import type 导入纯类型

错误(Agent 会这样做):

import { User, UserService } from "./user";
// User is only used as a type, but gets included in the bundle

正确:

import type { User } from "./user";
import { UserService } from "./user";

原因: import type 会在编译时被移除,从而减小打包体积。它也能明确表达意图——此导入仅用于类型。

12. 使用 Record 代替索引签名

错误(Agent 会这样做):

interface Config {
  [key: string]: string;
}

正确:

type Config = Record<string, string>;

// Or better - use a specific union for keys:
type Config = Record<"host" | "port" | "env", string>;

原因: 与索引签名相比,Record<K, V> 更易读,也更易组合。在可能的情况下,使用联合类型表示键,以进行穷尽检查。

13. 使用 using 管理资源

错误(Agent 会这样做):

const file = openFile("data.txt");
try {
  processFile(file);
} finally {
  file.close();
}

正确:

using file = openFile("data.txt");
processFile(file);
// file.close() called automatically via Symbol.dispose

原因: using 关键字(TypeScript 5.2+)通过 Disposable 协议提供确定性的资源清理,类似于 Python 的 with 或 C# 的 using。

模式

  • 在每个项目中启用 strict: true 和 noUncheckedIndexedAccess: true
  • 使用 satisfies 验证类型,同时避免类型扩大
  • 使用带有 type 或 kind 字段的可辨识联合来建模状态
  • 对配置对象和路由映射使用 as const
  • 对特定领域的 ID 使用品牌类型
  • 对所有仅用于类型的导入使用 import type
  • 使用带有 never 默认分支的穷尽式 switch 处理联合类型

反模式

  • 绝不使用 any——使用 unknown,并通过类型守卫缩小类型范围
  • 除非确实掌握比编译器更多的信息,否则绝不使用 as 进行类型断言
  • 绝不使用可选字段表示互斥状态——应使用可辨识联合
  • 绝不使用 // @ts-ignore 或 // @ts-expect-error,除非添加注释说明原因
  • 绝不使用 enum——应改用 as const 对象或联合类型
  • 绝不使用 Function 类型——应使用具体的函数签名
  • 绝不为图方便而禁用严格模式

评论

支持 Markdown