关于 typescript-best-practices
面向处理 TypeScript 代码的 AI Agent,介绍 TypeScript 5.x 的现代模式,帮助增强类型安全并在编译时发现错误。
主要功能
- 01启用严格编译选项
- 02使用 unknown 并收窄类型
- 03优先用 satisfies 替代断言
- 04用可辨识联合建模状态
- 05检查 switch 分支完整性
- 06用品牌类型保护 ID
适用场景
- 01检查 TypeScript 代码中的不安全模式
- 02设计类型化配置和 API 状态
- 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类型——应使用具体的函数签名 - 绝不为图方便而禁用严格模式