第 5 章:工程化实践¶
学习时间:3 小时 | 难度:⭐⭐⭐ 中级 | 前置知识:第 1 章
本章概览¶
TypeScript 工程化是大型项目保持代码质量的关键。本章覆盖 ESLint 配置、测试框架 Vitest、monorepo 管理与 CI/CD 集成。
学习目标:
- 配置
typescript-eslint进行代码质量检查 - 掌握 Vitest 写类型安全的单元测试
- 理解 monorepo 与 pnpm workspaces
- 配置 CI/CD 自动化类型检查
- 掌握
.d.ts声明文件编写
5.1 ESLint + typescript-eslint 配置¶
JavaScript
// eslint.config.mjs(扁平配置格式,ESLint 8+ 默认)
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
// 基础推荐规则
eslint.configs.recommended,
// TypeScript 严格规则(强烈推荐)
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
// 类型感知规则需要提供 tsconfig
languageOptions: {
parserOptions: {
projectService: true, // 自动发现 tsconfig.json
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// 关闭与 TypeScript 冲突的规则
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
argsIgnorePattern: "^_", // 允许 _param 形式的未使用参数
varsIgnorePattern: "^_",
}],
// 禁止 any(允许显式 as unknown as T 转换)
"@typescript-eslint/no-explicit-any": "error",
// 要求异步函数返回 Promise 时使用 await 或返回类型明确
"@typescript-eslint/require-await": "error",
"@typescript-eslint/no-floating-promises": "error", // 必须处理 Promise
// 命名约定
"@typescript-eslint/naming-convention": [
"error",
{ selector: "interface", format: ["PascalCase"] },
{ selector: "typeAlias", format: ["PascalCase"] },
{ selector: "enum", format: ["PascalCase"] },
{ selector: "variable", format: ["camelCase", "UPPER_CASE", "PascalCase"] },
],
},
},
// 忽略文件
{
ignores: ["dist/**", "node_modules/**", "*.config.{js,mjs}"],
}
);
5.2 Vitest 测试框架¶
Vitest 是专为 Vite/TypeScript 设计的测试框架,开箱即用支持 TS,比 Jest 快 3-5 倍:
TypeScript
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // 全局 describe/it/expect(无需导入)
environment: "node", // 或 "jsdom"(浏览器环境)
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
},
},
},
});
TypeScript
// src/utils/validator.ts
export function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function validateAge(age: number): { valid: boolean; message?: string } {
if (age < 0) return { valid: false, message: "Age cannot be negative" };
if (age > 150) return { valid: false, message: "Age seems unrealistic" };
return { valid: true };
}
// src/utils/validator.test.ts
import { describe, it, expect } from "vitest";
import { validateEmail, validateAge } from "./validator";
describe("validateEmail", () => {
it("should accept valid emails", () => {
expect(validateEmail("user@example.com")).toBe(true);
expect(validateEmail("user+tag@domain.co.uk")).toBe(true);
});
it("should reject invalid emails", () => {
expect(validateEmail("not-an-email")).toBe(false);
expect(validateEmail("@nodomain.com")).toBe(false);
expect(validateEmail("spaces in@email.com")).toBe(false);
});
});
describe("validateAge", () => {
it("should return valid for reasonable ages", () => {
expect(validateAge(25)).toEqual({ valid: true });
expect(validateAge(0)).toEqual({ valid: true });
});
it("should return error for negative age", () => {
expect(validateAge(-1)).toEqual({
valid: false,
message: "Age cannot be negative",
});
});
it("should return error for unrealistic age", () => {
const result = validateAge(200);
expect(result.valid).toBe(false);
expect(result.message).toContain("unrealistic");
});
});
类型测试(expectTypeOf)¶
TypeScript
// src/types.test.ts
import { expectTypeOf, assertType } from "vitest";
import { type UserPublic } from "./types";
// 确保类型转换/工具类型的输出类型正确
it("UserPublic should not include password", () => {
expectTypeOf<UserPublic>().not.toHaveProperty("password");
expectTypeOf<UserPublic>().toHaveProperty("name");
expectTypeOf<UserPublic["name"]>().toBeString();
});
// 确保函数返回类型正确
it("parseId should return number", () => {
assertType<number>(parseId("42"));
});
5.3 pnpm Monorepo 与 Workspaces¶
Text Only
my-monorepo/
├── pnpm-workspace.yaml
├── package.json # 根 package.json(工具脚本)
├── tsconfig.base.json # 共享 tsconfig
├── packages/
│ ├── shared-types/ # @myapp/shared-types
│ ├── ui/ # @myapp/ui
│ └── utils/ # @myapp/utils
└── apps/
├── web/ # Next.js 前端
└── api/ # Fastify 后端
Text Only
// tsconfig.base.json(所有子包继承)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
Text Only
// packages/shared-types/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
Text Only
// packages/shared-types/package.json
{
"name": "@myapp/shared-types",
"version": "0.1.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
5.4 声明文件(.d.ts)编写¶
当你需要为没有类型定义的 JS 库、环境变量或全局对象添加类型时:
TypeScript
// src/env.d.ts — 类型化环境变量(与实际配置同步!)
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production" | "test";
readonly DATABASE_URL: string;
readonly REDIS_URL: string;
readonly JWT_SECRET: string;
readonly API_KEY: string;
// PORT 可能未设置
readonly PORT?: string;
}
}
// 使用时有完整类型提示
const dbUrl = process.env.DATABASE_URL; // string(不是 string | undefined!)
const port = process.env.PORT ?? "3000"; // string | undefined → string(有默认值)
// src/global.d.ts — 全局类型扩展
declare global {
// 扩展全局对象
interface Window {
gtag: (...args: unknown[]) => void;
dataLayer: Record<string, unknown>[];
}
// 全局工具类型(在整个项目中可用,无需导入)
type Maybe<T> = T | null | undefined;
type AsyncFn<T = void> = () => Promise<T>;
}
// 模块增强:给已有模块添加类型
import "express";
declare module "express" {
interface Request {
user?: { id: string; role: "admin" | "user" };
requestId: string;
}
}
5.5 CI/CD 类型检查配置¶
YAML
# .github/workflows/typecheck.yml
name: Type Check & Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- run: pnpm install --frozen-lockfile
# 类型检查(严格模式)
- name: TypeScript Type Check
run: pnpm tsc --noEmit
# ESLint 检查
- name: ESLint
run: pnpm eslint src --max-warnings 0
# 运行测试 + 覆盖率
- name: Unit Tests
run: pnpm vitest run --coverage
# 上传覆盖率报告
- uses: codecov/codecov-action@v4
with:
files: coverage/lcov.info
📌 本章小结¶
| 工具 | 用途 | 要点 |
|---|---|---|
| typescript-eslint | 代码质量 | strictTypeChecked 规则集,类型感知检查 |
| Vitest | 测试 | 原生 TS,expectTypeOf 做类型测试 |
| pnpm workspaces | Monorepo | 统一版本管理,共享 tsconfig |
.d.ts 文件 | 类型声明 | 环境变量、全局类型、模块增强 |
| GitHub Actions | CI/CD | tsc --noEmit + ESLint + Vitest |
TypeScript 5.8 · 2025