← 返回博客

TypeScript 工程实践:从类型系统到项目架构的落地方法

TypeScript 的类型系统是它最强大的武器,但大多数项目只用了它不到一半的能力。本文从类型体操、错误处理、项目结构、配置策略四个层面,给出 TypeScript 项目从"能用"到"好维护"的工程实践——适合正在或准备用 TypeScript 构建中大型项目的团队。

先说结论:TypeScript 的优势不在”类型”,而在”可维护性”

很多团队用 TypeScript 只是因为”大家都在用”,但实际项目中 any 满天飞,类型定义和实际代码不一致,TypeScript 变成了”加了类型检查的 JavaScript”——该出的 bug 一个没少。

本文从四个层面展开:类型系统、错误处理、项目结构、配置策略。


1. 类型系统:用好 discriminated union 和类型收窄

1.1 用 discriminated union 替代可选字段

这是最常见的反模式:用一个对象类型,里面全是可选字段,运行时判断字段是否存在。

// ❌ 反模式
type ApiResponse = {
  success?: boolean;
  data?: any;
  error?: string;
};

// ✅ 推荐:discriminated union
type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

discriminated union 的好处是:TypeScript 会在类型收窄后自动推断出可用的字段,不需要手动判断。

const res: ApiResponse<User> = await fetchUser();
if (res.success) {
  console.log(res.data.name); // ✅ TypeScript 知道 data 存在
} else {
  console.log(res.error);     // ✅ TypeScript 知道 error 存在
}

1.2 类型收窄的几种方式

// typeof 收窄(原始类型)
if (typeof x === 'string') { /* x: string */ }

// instanceof 收窄(类实例)
if (x instanceof Error) { /* x: Error */ }

// in 操作符收窄(对象属性)
if ('error' in x) { /* x: 有 error 属性的类型 */ }

// 自定义类型守卫
function isUser(x: any): x is User {
  return x && typeof x.id === 'number' && typeof x.name === 'string';
}

2. 错误处理:用 Result 模式替代 throw

2.1 为什么不用 throw

throw 的问题在于:TypeScript 不强制调用方处理异常。一个函数 throws 了,但调用方可能忘了写 try-catch,程序会在运行时崩溃。

2.2 Result 模式实现

type Result<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseJSON(json: string): Result<unknown> {
  try {
    return { ok: true, value: JSON.parse(json) };
  } catch (e) {
    return { ok: false, error: e as Error };
  }
}

// 调用方必须处理两种情况
const result = parseJSON('{"name":"test"}');
if (result.ok) {
  console.log(result.value); // ✅ 类型安全
} else {
  console.error(result.error.message); // ✅ 必须处理错误
}

2.3 顶层统一处理

在 API 路由或事件处理函数的顶层,将 Result 统一转换为响应:

app.get('/users', async (req, res) => {
  const result = await getUsers();
  if (!result.ok) {
    return res.status(500).json({ error: result.error.message });
  }
  res.json(result.value);
});

3. 项目结构:分层清晰的类型定义

3.1 推荐目录结构

src/
  types/           # 全局共享类型
    user.ts
    api.ts
    common.ts
  api/             # API 调用层
    types.ts       # 接口对应的类型(自动生成)
    client.ts
  utils/           # 工具函数
    result.ts      # Result 类型定义
  components/      # 组件
    UserCard.tsx

3.2 类型文件只放类型

不要在类型文件中写实现代码,哪怕只是一个简单的函数。类型文件和实现文件分离,职责清晰。


4. 配置策略

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

总结

层面核心原则常见错误
类型系统用 discriminated union 替代可选字段any 满天飞
错误处理Result 模式替代 throw漏掉 try-catch
项目结构类型和实现分离类型文件中写实现代码
配置strict: true逐项关闭严格检查

TypeScript 的价值不在于”写了类型”,而在于”类型帮你发现了你没想到的 bug”。 如果配置了 strict 模式、用了 discriminated union 和 Result 模式,TypeScript 的收益会远远大于它的学习成本。

需要 TypeScript 项目搭建或技术咨询?联系我们,说清你的项目规模与团队情况,24 小时内回可行性。

相关阅读

常见问题

any 类型到底能不能用?

能,但要有明确的使用场景和边界。建议:① 与第三方库交互时,如果对方没有类型定义,用 any 并限制在模块边界内,不扩散到业务代码;② 迁移 JavaScript 项目时,用 any 作为过渡,逐步替换为具体类型;③ 绝对不要在函数返回值上用 any——这会污染所有调用方的类型推断。推荐使用 unknown 替代 any,因为 unknown 在访问前必须做类型收窄,更安全。

tsconfig.json 里最关键的配置项是什么?

strict: true 是最重要的一行配置。它同时开启了 strictNullChecks、noImplicitAny、strictFunctionTypes 等多项检查,能拦截大量潜在 bug。除此之外,建议开启 noUnusedLocals 和 noUnusedParameters 来避免死代码,以及 exactOptionalPropertyTypes 来防止误写可选属性。

类型定义应该放在哪里?

推荐分层策略:① 全局共享类型放在 src/types/ 目录下,按模块分文件;② 组件或函数的局部类型就近定义,不放到全局类型文件中;③ 与 API 接口对应的类型放在 api/types.ts 中,从 OpenAPI spec 自动生成;④ 避免在类型文件中写实现代码,类型文件只放类型。

TypeScript 项目应该用什么错误处理模式?

推荐 Result 模式( discriminated union ),而不是 throw。定义一个 type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E },每个可能失败的函数都返回 Result 类型。调用方必须处理 ok 和 error 两种情况,不会漏掉错误处理。在顶层(如 API 路由处理函数)统一将 Result 转换为 HTTP 响应。

本文来自 AI Enable Harness 一线交付实践。需要同类系统或优化服务?

📡 本文同步发布平台: CSDN 知乎

订阅博客更新

新文章发布后第一时间邮件通知。不定期发送,不推销。

订阅 →