Skip to content

工程实践概览

前面几篇讲的是语言本身,这一篇讲类型在真实项目里怎么用

这一篇解决什么问题

  • 框架(React / Vue)里的类型怎么写才不别扭
  • Node 服务端(Express / Fastify)的类型边界在哪
  • tsc / esbuild / tsx / tsup / tsdown / vite 各自管哪一段(最容易混的一块)
  • 异步迭代器、流式响应怎么建模
  • ESM / CJS 与模块解析的那些坑
  • 发布一个带类型的 npm 包要配什么

一个贯穿全文的原则

类型只覆盖编译期,运行时是另一回事。

ts
interface User {
  name: string
  age: number
}

// 这个断言在运行时完全不生效
const user = JSON.parse(input) as User

JSON.parse 返回 anyas User 只是让编译器闭嘴。外部输入必须做运行时校验,然后用 schema 反推类型:

ts
import { z } from 'zod'

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
})

type User = z.infer<typeof UserSchema>

const user = UserSchema.parse(JSON.parse(input)) // 校验 + 类型都对

这个方向很重要:类型是推导出来的,不是手写的。手写两遍(schema 一遍、类型一遍)一定会不同步。

类型边界的划分

一个健康的项目,类型边界长这样:

外部输入 → 运行时校验 → 内部类型(可信)→ 业务逻辑
   ↑                                          ↓
  unknown                                  内部输出

规则:

  1. 所有外部输入先当作 unknown
  2. 边界处做一次运行时校验,之后内部代码可以完全信任类型
  3. 不要在每个函数里重复判空
ts
// 边界:校验 + 收窄
async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  const data: unknown = await res.json()
  return UserSchema.parse(data) // 这里之后类型可信
}

// 内部:直接信类型,不再判空
function greet(u: User): string {
  return `hi ${u.name}`
}

常见反模式

1. any 传染

ts
function f(x: any) {
  return x.a.b.c // any 一路传下去,整个调用链都失去检查
}

2. 到处 !

ts
// 每个字段都加 ! 等于关掉 strictNullChecks
const name = user!.profile!.name!

3. 过度抽象的类型

ts
type Handler<T extends Record<string, unknown>, K extends keyof T> = ...

如果团队里只有一个人看得懂,维护成本会超过收益。

4. 手写 schema 对应的类型

ts
// schema 改了,这个类型不会自动跟着改
type User = { name: string; age: number }
const UserSchema = z.object({ name: z.string(), age: z.number(), email: z.string() })

永远用 z.infer 之类的方式推导。

各章导航

章节适合谁
工具链分工搞不清 tsc / tsx / tsup / vite 谁管什么
React 与 Vue 中的类型写前端组件
Node 与服务端类型写后端
异步与迭代器处理流、生成器
流式与 SSE接 AI / 实时数据
ESM / CJS 与模块解析配构建、发包
发布带类型的包维护 npm 包

下一步

基于 VitePress 与 Twoslash 构建