typescript-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen to use
适用场景
Use this skill when working with TypeScript code. AI agents frequently generate outdated patterns -
using instead of , type assertions instead of , optional fields instead of
discriminated unions, and missing strict mode options. This skill enforces modern TypeScript 5.x
patterns.
anyunknownsatisfies在编写TypeScript代码时使用本技能。AI Agent经常生成过时的模式——用而非,用类型断言而非,用可选字段而非区分联合类型,还会遗漏严格模式配置。本技能可强制采用TypeScript 5.x的现代模式。
anyunknownsatisfiesCritical 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. prevents unsafe
array/object access. Agents often skip these for "convenience."
noUncheckedIndexedAccess错误写法(Agent常这么写):
json
{
"compilerOptions": {
"strict": false,
"target": "ES2020"
}
}正确写法:
json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"target": "ES2022"
}
}原因: 严格模式可以排查整类bug。能防止不安全的数组/对象访问。Agent常常为了“省事”跳过这些配置。
noUncheckedIndexedAccess2. 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 stringCorrect:
typescript
const config = {
port: 3000,
host: "localhost",
} satisfies Config;
config.port.toFixed(); // TypeScript knows port is numberWhy: validates the type without widening it. silences the compiler and can hide
bugs. Use for validation, only when you genuinely know more than the compiler.
satisfiesassatisfiesas错误写法(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类型原因: 在验证类型的同时不会拓宽类型。会让编译器静默,可能隐藏bug。用做类型验证,只有当你确实比编译器更了解类型时才使用。
satisfiesassatisfiesas3. 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 , TypeScript widens literal types to . With it, you get exact
literal types and readonly properties.
as conststring错误写法(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" }原因: 没有时,TypeScript会把字面量类型拓宽为。使用它之后,你会得到精确的字面量类型和只读属性。
as conststring5. 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 crashCorrect:
typescript
function parseJson(text: string): unknown {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
if (isUser(data)) {
data.name; // Safe - type narrowed
}Why: disables all type checking. forces you to narrow the type before using it,
catching bugs at compile time.
anyunknown错误写法(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; // 安全——类型已收窄
}原因: 会禁用所有类型检查。会强制你在使用前收窄类型,在编译时就能发现bug。
anyunknown6. 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 CCorrect:
typescript
function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // Error - 'purple' not in CWhy: (TypeScript 5.4+) prevents a parameter from influencing type inference,
ensuring stricter checks.
NoInfer<T>错误写法(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中原因: (TypeScript 5.4+)可以防止参数影响类型推断,确保更严格的检查。
NoInfer<T>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 OrderIdWhy: 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 check ensures every union member is handled. When a new status is added, the
compiler flags the missing case.
never错误写法(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}`);
}
}
}原因: 检查确保联合类型的每个成员都被处理。当添加新状态时,编译器会标记缺失的分支。
never10. 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 () narrow types safely with runtime checks. Type assertions
() bypass the compiler and can hide bugs.
item is Useras User错误写法(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
}
}原因: 类型谓词()通过运行时检查安全地收窄类型。类型断言()会绕过编译器,可能隐藏bug。
item is Useras User11. 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 bundleCorrect:
typescript
import type { User } from "./user";
import { UserService } from "./user";Why: is erased at compile time, reducing bundle size. It also makes the intent
clear - this import is for types only.
import type错误写法(Agent常这么写):
typescript
import { User, UserService } from "./user";
// User仅作为类型使用,但会被打包进产物正确写法:
typescript
import type { User } from "./user";
import { UserService } from "./user";原因: 在编译时会被移除,减小包体积。同时也能明确表达意图——这个导入仅用于类型。
import type12. 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: is more readable and composable than index signatures. When possible, use a
union for keys to get exhaustive checking.
Record<K, V>错误写法(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.disposeWhy: The keyword (TypeScript 5.2+) provides deterministic resource cleanup via the
Disposable protocol, similar to Python's or C#'s .
usingwithusing错误写法(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自动调用原因: 关键字(TypeScript 5.2+)通过Disposable协议提供确定性的资源清理,类似于Python的或C#的。
usingwithusingPatterns
推荐模式
- Enable and
strict: truein every projectnoUncheckedIndexedAccess: true - Use for type validation without widening
satisfies - Use discriminated unions with a or
typefield for state modelingkind - Use for configuration objects and route maps
as const - Use branded types for domain-specific IDs
- Use for all type-only imports
import type - Use exhaustive with
switchdefault for union handlingnever
- 在所有项目中启用和
strict: truenoUncheckedIndexedAccess: true - 使用进行类型验证且不拓宽类型
satisfies - 对状态建模时,使用带有或
type字段的区分联合类型kind - 对配置对象和路由映射使用
as const - 对领域特定ID使用品牌类型
- 对所有仅类型导入使用
import type - 处理联合类型时,结合默认分支使用穷尽
never语句switch
Anti-Patterns
反模式
- NEVER use - use
anyand narrow with type guardsunknown - NEVER use for type assertions unless you genuinely know more than the compiler
as - NEVER use optional fields to model mutually exclusive states - use discriminated unions
- NEVER use or
// @ts-ignorewithout a comment explaining why// @ts-expect-error - NEVER use - use
enumobjects or union types insteadas const - NEVER use type - use specific function signatures
Function - NEVER disable strict mode for convenience
- 绝对不要使用——用
any并结合类型守卫收窄类型unknown - 绝对不要用做类型断言,除非你确实比编译器更了解类型
as - 绝对不要用可选字段建模互斥状态——使用区分联合类型
- 绝对不要在没有注释说明原因的情况下使用或
// @ts-ignore// @ts-expect-error - 绝对不要使用——改用
enum对象或联合类型as const - 绝对不要使用类型——改用特定的函数签名
Function - 绝对不要为了省事而禁用严格模式