跳转到内容

Typescript开发规范

TS 类型、接口、泛型


  • [Requirement] 必须启用 strict 模式("strict": true),禁止隐式 any;显式 any 必须在代码行附注释说明无法精确定义的原因。
  • [Requirement] 禁止以 as any 或 as unknown as X 绕过类型检查;使用 unknown 作为未知类型的默认占位,再通过类型守卫窄化。
  • [Requirement] 函数必须显式标注返回类型,避免返回值类型随实现漂移导致的隐式 breaking change。
  • [Standard] 基本类型统一使用小写:string、number、boolean、null、undefined、bigint、symbol;禁止使用大写构造器 String、Number、Boolean 作为类型。
  • [Standard] 数组使用 T[] 写法,仅在复杂泛型或联合类型场景使用 Array<T>;元组使用 [T, U] 明确位置与长度。
  • [Standard] 可空值使用 T | null;“无值”语义用 null,“未初始化/可选”语义用 undefined,避免两者混用。
  • [Standard] 有限状态集使用字符串字面量联合类型:type Status = 'idle' | 'loading' | 'success' | 'error',而非数值枚举。
  • [Default] 使用 const enum 或字符串字面量联合替代常规数值 enum,避免生成运行时反向映射对象;对外暴露 API 时优先字面量联合。
  • [Default] 对既有变量做结构校验又希望保留字面量类型时使用 satisfies,不使用 as 断言。
  • [Suggestion] 复杂类型提取为具名 type 别名,不内联嵌套超过 2 层的联合/交叉类型。
  • [Caution] // @ts-ignore / // @ts-expect-error 必须在下一行附带说明注释,并优先尝试 @ts-expect-error(若错误消失会被 TS 报错,更安全)。
// 推荐:字面量联合 + 显式返回类型
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'completed' | 'cancelled';
 
interface Order {
  id: string;
  status: OrderStatus;
  paidAt: number | null;
  items: OrderItem[];
}
 
function getOrderStatus(order: Order): OrderStatus {
  return order.status;
}
 
// 推荐:unknown + 类型守卫,替代 any
function parsePayload(raw: unknown): Order | null {
  if (typeof raw !== 'object' || raw === null) return null;
  const o = raw as Record<string, unknown>;
  if (typeof o.id !== 'string' || typeof o.status !== 'string') return null;
  return o as Order;
}
 
// 推荐:satisfies 保留字面量类型
const HTTP_STATUS = {
  OK: 200,
  NOT_FOUND: 404,
} as const;
// HTTP_STATUS.OK 的类型为 200 而非 number
 
const config = {
  retries: 3,
  timeout: 5000,
} satisfies { retries: number; timeout: number };
// 反模式:隐式 any
function fetchUser(id) {          // 参数 id 隐式 any
  return axios.get(`/users/${id}`);
}
 
// 反模式:as any 绕过检查
const data = response.data as any;
data.users.map(u => u.name);      // 完全丢失类型保护
 
// 反模式:用 Number/String 作为类型
const count: Number = 10;         // 应使用 number
const name: String = 'x';         // 应使用 string
 
// 反模式:数值枚举产生运行时反向映射
enum Status { Idle, Loading }     // 生成 Status[0]='Idle' 的反向对象
Status[0]; // 'Idle',污染运行时
 
// 反模式:ts-ignore 无说明
// @ts-ignore
const x = foo.bar;                // 未说明为何忽略

  • [Requirement] 定义对象结构契约时优先使用 interface,便于声明合并与 extends;联合类型、映射类型、工具类型使用 type。
  • [Requirement] 对外暴露的 API 类型(DTO、请求/响应体)必须定义为 interface,且字段不可变时使用 readonly。
  • [Requirement] 接口字段数量超过 15 个或存在明显分组时,按职责拆分为多个小接口再组合,不做”上帝接口”。
  • [Standard] 接口命名使用大驼峰;表示能力/行为的接口以 I 前缀或 able 后缀命名(如 IDisposable、Serializable),DTO 不加前缀。
  • [Standard] 可选字段使用 ?:;不允许既用 ?: 又在类型中包含 undefined(field?: string | undefined 冗余)。
  • [Standard] 使用 extends 组合继承,不使用交叉类型 & 做接口合并(交叉类型在错误信息中更难读)。
  • [Default] 只读字段使用 readonly 修饰;明确不允许修改的集合使用 ReadonlyArray<T> 或 readonly T[]。
  • [Default] 方法签名在接口中使用方法简写 foo(): void,属性函数字面量用 foo: () => void,二者在 this 绑定上有差异需区分。
  • [Suggestion] 为接口补充 JSDoc,说明每个字段的业务含义、单位、是否可空。
  • [Caution] 禁止在接口中定义实现细节(如具体算法常量、内部方法),接口只描述行为契约。
// 推荐:拆分 + 组合
interface BaseEntity {
  readonly id: string;
  readonly createdAt: number;
  updatedAt: number;
}
 
interface UserProfile {
  name: string;
  avatarUrl: string | null;
}
 
interface User extends BaseEntity, UserProfile {
  email: string;
  roles: readonly Role[];
}
 
// 推荐:能力接口 + DTO 分离
interface IDisposable {
  dispose(): void;
}
 
interface CreateUserRequest {
  /** 用户昵称,1-32 字符 */
  name: string;
  /** 邮箱,需符合 RFC 5322 */
  email: string;
  /** 初始角色,空数组表示无角色 */
  roles?: Role[];
}
// 反模式:上帝接口
interface UserDTO {
  id: string;
  name: string;
  email: string;
  password: string;       // 响应体不应包含密码
  createdAt: number;
  updatedAt: number;
  token: string;          // 鉴权信息混入数据契约
  permissions: string[];
  // ... 超过 30 个字段
}
 
// 反模式:冗余 undefined
interface Foo {
  bar?: string | undefined;   // ?: 已包含 undefined
}
 
// 反模式:interface 用于联合类型
interface Result = Success | Failure;   // 错误语法,应用 type
 
// 反模式:接口中混入实现
interface Service {
  RETRY_TIMES = 3;           // 接口中不能有实现
  fetch(): void;
}

  • [Requirement] 工具函数、集合操作、高阶组件必须使用泛型而非 any,保证调用方类型安全。
  • [Requirement] 泛型参数命名使用大驼峰并具名(TData、TError、TValue),不使用单个字母 T、U 当参数超过 1 个时。
  • [Requirement] 对泛型参数有约束时使用 extends,如 <T extends object>,避免无约束泛型退化为 unknown。
  • [Standard] 默认泛型参数使用 = 指定默认类型,减少调用方显式传参:<T = unknown>。
  • [Standard] 泛型函数优先让 TS 从参数推断类型,调用方不显式传参;仅在推断失败时显式指定。
  • [Standard] 使用条件类型 T extends U ? X : Y、映射类型 { [K in keyof T]: ... }、infer 抽取类型时,需配套具名 type 别名以便阅读。
  • [Default] 公共库的泛型 API 必须提供至少一个使用示例的类型测试(expectType / tsd)。
  • [Suggestion] 泛型嵌套超过 3 层时拆分为中间类型别名,避免”类型俄罗斯套娃”。
  • [Caution] 禁止用 any 作为泛型参数的默认值(T = any),应使用 unknown。
// 推荐:具名泛型 + 约束 + 默认值
interface ApiResponse<TData = unknown, TError = unknown> {
  code: number;
  data: TData | null;
  error: TError | null;
}
 
async function request<TData = unknown>(
  url: string,
  options?: RequestInit,
): Promise<ApiResponse<TData>> {
  const res = await fetch(url, options);
  return res.json() as Promise<ApiResponse<TData>>;
}
 
// 调用方类型自动推断,无需显式传参
const users = await request<User[]>('/api/users');
// users.data 类型为 User[] | null
 
// 推荐:映射类型构造只读版本
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 反模式:泛型退化为 any
function first<T>(arr: any[]): any {   // 未使用泛型参数
  return arr[0];
}
 
// 反模式:单字母泛型,超过 1 个时难读
function merge<T, U, V>(a: T, b: U, c: V) { }
 
// 反模式:默认 any
function identity<T = any>(x: T): T { return x; }  // 应为 unknown
 
// 反模式:过度嵌套,无中间别名
type A = { [K in keyof B]: B[K] extends (...a: infer P) => infer R ? { params: P; result: R } : never };

  • [Requirement] 优先使用类型守卫(typeof、instanceof、in、自定义 is 谓词)窄化类型,而非直接断言。
  • [Requirement] as 断言只能在「类型存在包含关系」时使用;无包含关系时必须通过 unknown 中转,并附注释说明数据来源可信。
  • [Requirement] 禁止使用非空断言 ! 处理可选值,应使用可选链 ?. 或显式判空;仅在 JSX ref 等明确场景谨慎使用。
  • [Standard] 自定义类型守卫使用 x is T 谓词,返回布尔值并在函数名中体现语义(isUser、isErrorResponse)。
  • [Standard] 对外部数据(API 响应、localStorage、postMessage)做断言前必须先运行时校验(zod、io-ts 或手写校验)。
  • [Default] 类型断言数量在单文件中超过 5 处时,需 Review 确认是否应抽离为类型守卫或校验函数。
  • [Suggestion] 用 satisfies 做结构校验,用 as const 做字面量收窄,用泛型函数做推断,组合减少 as 的使用。
  • [Caution] JSON.parse 返回值为 any,禁止直接当作业务类型使用,必须经校验后再断言。
// 推荐:自定义类型守卫
interface Dog { bark(): void; }
interface Cat { meow(): void; }
 
function isDog(animal: Dog | Cat): animal is Dog {
  return 'bark' in animal;
}
 
function speak(animal: Dog | Cat) {
  if (isDog(animal)) {
    animal.bark();
  } else {
    animal.meow();
  }
}
 
// 推荐:运行时校验后再断言
import { z } from 'zod';
 
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});
 
async function loadUser(id: string): Promise<User> {
  const raw = await fetch(`/users/${id}`).then(r => r.json());
  const parsed = UserSchema.parse(raw); // 运行时校验,失败抛错
  return parsed; // 类型自动为 { id: string; name: string }
}
// 反模式:直接断言外部数据
const user = JSON.parse(localStorage.getItem('user')!) as User;
// 数据可能被篡改或结构变化,无任何运行时校验
 
// 反模式:非空断言掩盖空值
function getLength(s?: string) {
  return s!.length;   // s 可能为 undefined
}
 
// 反模式:无包含关系直接断言
const num = 42 as unknown as string;  // 通过 unknown 中转但数据不可信
 
// 反模式:用 as 替代类型守卫
(animal as Dog).bark();   // 若 animal 实际是 Cat,运行时崩溃

  • [Requirement] tsconfig.json 必须设置 "strict": true,并显式开启 noImplicitAny、strictNullChecks、noUncheckedIndexedAccess(项目可接受时)。
  • [Requirement] 必须开启 "noUnusedLocals": true 与 "noUnusedParameters": true,未使用变量用 _ 前缀。
  • [Requirement] 必须开启 "noFallthroughCasesInSwitch": true,switch 每个 case 必须 break / return / throw。
  • [Requirement] target 不低于 ES2020,module 与构建工具匹配(Vite 用 ESNext,Node 用 NodeNext/CommonJS)。
  • [Standard] 开启 "exactOptionalPropertyTypes": true,可选属性不允许显式赋值 undefined。
  • [Standard] 开启 "noImplicitReturns": true,函数所有分支必须有返回值。
  • [Standard] 使用 "paths" 配置路径别名,并同步到 vite.config.ts / jest.config.ts / 编辑器。
  • [Default] 开启 "forceConsistentCasingInFileNames": true,避免大小写不一致导致的跨平台问题。
  • [Default] 开启 "isolatedModules": true,保证类型可被独立转译(适配 Vite/esbuild)。
  • [Suggestion] 使用 tsconfig.base.json + 项目级 tsconfig.json 继承,monorepo 共享基础配置。
  • [Caution] 禁止在 tsconfig.json 中关闭 strict 或其子项以”让编译通过”,问题应在代码中解决。
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}
# 类型检查加入 CI 与 pre-commit
npx tsc --noEmit
// 反模式:关闭严格模式
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": false,
    "strictNullChecks": false
  }
}
// 反模式:未使用参数不加下划线
function handle(x: number, y: number) {  // y 未使用且无 _ 前缀
  return x * 2;
}
 
// 反模式:switch 穿透
function handleStatus(s: Status) {
  switch (s) {
    case 'loading':
      showSpinner();
      // 缺少 break,会继续执行下面的 case
    case 'error':
      showError();
  }
}