← Frontend / 全栈实践

02_TypeScript与应用架构

判别联合、运行时数据边界、DTO 映射与按功能组织前端代码。

TypeScript 与前端应用架构

学习目标:把已有 TypeScript 语法用于真实应用的数据边界、模块组织和错误建模。

1. 类型不能代替运行时校验

TypeScript 类型在编译后消失。fetch 返回的 JSON、浏览器存储、URL 参数都属于不可信输入;需要运行时检查或受约束的 schema,不能直接 as User 就认定安全。

type LoadState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string };

function titleOf(state: LoadState<{ title: string }>): string {
  if (state.status === 'success') return state.data.title;
  if (state.status === 'error') return state.message;
  return '加载中';
}

判别联合把互斥状态写进类型,避免 data、loading、error 三个布尔/可空字段出现矛盾组合。

2. 把服务端 DTO 转成界面模型

HTTP 客户端 → 运行时解析 → DTO → 映射函数 → 页面模型 → 组件

接口字段可能使用字符串时间、可空值或后端命名规则;组件不应到处重复转换。集中封装 api/ 和映射函数,让 UI 面对稳定的数据模型。API 契约仍以服务端文档与契约测试为准。

3. 按功能组织代码

src/
  app/          # 路由、全局提供者、入口
  features/notes/
    api/        # 请求与 DTO
    components/ # 笔记界面
    model/      # 类型与业务映射
  shared/       # 经多个功能验证确实共用的组件

先按业务功能聚合,再提炼共用层;不要一开始把所有代码分散进巨大的 components/、utils/。跨层引用保持单向,避免组件反过来直接修改全局数据源。

4. 严格模式与错误处理

开启 TypeScript strict,少用 any;边界值优先用 unknown,检查后再使用。网络错误、超时、取消和业务错误要有不同处理;不要把所有失败转成空数组,否则页面会把故障误显示为“没有数据”。

Tip:把类型检查失败看成“契约不清楚”的信号,先核对数据流,再考虑类型断言。

自测

  1. const user = json as User 会验证服务端响应吗?不会。
  2. 为什么按功能聚合代码?相关页面、API、模型更容易一起定位和修改。

5. 在网络边界从 unknown 开始

response.json() 得到的数据可能缺字段、字段类型错误或来自旧版本服务。定义一个小的解析函数,比在组件里到处写 as Note 更可靠:

type Note = { id: number; title: string };

function parseNote(input: unknown): Note {
  if (typeof input !== 'object' || input === null) throw new Error('Invalid note');
  const value = input as Record<string, unknown>;
  if (typeof value.id !== 'number' || !Number.isInteger(value.id))
    throw new Error('Invalid note id');
  if (typeof value.title !== 'string') throw new Error('Invalid note title');
  return { id: value.id, title: value.title };
}

这里断言成 Record<string, unknown> 只是为了安全读取字段,每个字段仍被检查。真实项目可使用受维护的 schema 库,规则集中放在 features/notes/api。若 API 的 ID 可能超过 JavaScript 安全整数范围,应在契约中决定是否以字符串传输。

6. 用类型表达失败分支

业务错误不是单一 Error 字符串。可将 unauthorized、forbidden、conflict、network 与 invalid_response 建模为判别联合,组件根据 kind 给出不同处理。每新增错误种类,switch 的穷尽检查会提示尚未处理的页面分支。

练习:写 parseNotes(input: unknown): Note[],先确认是数组,再逐项调用 parseNote;分别测试空数组、缺失 id、字符串 id 和正常响应。然后把列表组件中的类型断言删掉。