TypeScript 技能
加载与:base.md
严格模式(不可协商)
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
项目结构
project/
├── src/
│ ├── core/ # 纯业务逻辑
│ │ ├── types.ts # 领域类型/接口
│ │ ├── services/ # 纯函数
│ │ └── index.ts # 公共API
│ ├── infra/ # 副作用
│ │ ├── api/ # HTTP处理器
│ │ ├── db/ # 数据库操作
│ │ └── external/ # 第三方集成
│ └── utils/ # 共享工具
├── tests/
│ ├── unit/
│ └── integration/
├── package.json
├── tsconfig.json
└── CLAUDE.md
工具(必需)
// package.json 脚本
{
"scripts": {
"lint": "eslint src/ --ext .ts,.tsx",
"typecheck": "tsc --noEmit",
"test": "jest",
"test:coverage": "jest --coverage",
"format": "prettier --write 'src/**/*.ts'"
}
}
// eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'max-lines-per-function': ['error', 20],
'max-depth': ['error', 2],
'max-params': ['error', 3],
}
}
);
使用 Jest 测试
// tests/unit/services/user.test.ts
import { calculateTotal } from '../../../src/core/services/pricing';
describe('calculateTotal', () => {
it('返回项目价格的总和', () => {
// 安排
const items = [{ price: 10 }, { price: 20 }];
// 行动
const result = calculateTotal(items);
// 断言
expect(result).toBe(30);
});
it('返回空数组为零', () => {
expect(calculateTotal([])).toBe(0);
});
it('对无效项目抛出异常', () => {
expect(() => calculateTotal([{ invalid: 'item' }])).toThrow();
});
});
GitHub Actions
name: TypeScript 质量门
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 设置 Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 安装依赖
run: npm ci
- name: 代码检查
run: npm run lint
- name: 类型检查
run: npm run typecheck
- name: 测试覆盖率
run: npm run test:coverage
- name: 覆盖率阈值(80%)
run: npm run test:coverage -- --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
预提交钩子
使用 Husky + lint-staged:
npm install -D husky lint-staged
npx husky init
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
# .husky/pre-commit
npx lint-staged
npx tsc --noEmit
npm run test -- --onlyChanged --passWithNoTests
这在每次提交时运行:
- ESLint + Prettier 在暂存文件上
- 整个项目的类型检查
- 仅针对更改的文件进行测试
类型模式
结果的区分联合
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
function parseUser(data: unknown): Result<User> {
// 无异常的类型安全错误处理
}
ID的品牌类型
type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };
// 不能意外地将 UserId 传递到 OrderId 预期的地方
function getOrder(orderId: OrderId): Order { ... }
字面量的 Const 断言
const STATUSES = ['pending', 'active', 'closed'] as const;
type Status = typeof STATUSES[number]; // 'pending' | 'active' | 'closed'
Zod 用于运行时验证
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
type User = z.infer<typeof UserSchema>;
TypeScript 反模式
- ❌
any类型 - 使用unknown并缩小 - ❌ 类型断言(
as) - 使用类型守卫 - ❌ 非空断言(
!) - 显式处理空值 - ❌
@ts-ignore无解释 - ❌ 枚举 - 使用 const 对象或联合类型
- ❌ 数据的类 - 使用接口/类型
- ❌ 默认导出 - 使用命名导出