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