# 类型与组件怎样帮助维护界面

原文: https://yishto.com/learn/web-interface/typescript-and-ui-frameworks/

作者: [Yishto](https://yishto.com/about/)

把外部数据验证、联合状态和组件所有权连接起来，理解 TypeScript 与 React、Vue 各自能检查哪些问题。

首次发布: 2026-11-10
内容更新: 2026-11-10

资料卡片最初只有标题，后来增加下载状态、错误提示和重试。若每个页面各自拼字段、改布尔值，很快会出现“加载中却显示旧错误”或“标题字段缺失仍进入成功页”。类型和组件帮助表达约束，但运行时的接口数据仍需要单独检查。

教学案例是一个资料列表组件：接收经过验证的记录，显示加载、成功或错误，并通过事件提出收藏请求。目标是让合法状态容易表达，让错误数据在进入界面前被拒绝。本课不给出未运行的框架项目，代码只验证合成状态契约。

## 静态类型不能替网络验证

TypeScript 在运行前检查程序中的类型关系，生成 JavaScript 时类型标记通常会被擦除。把接口结果断言为某个类型，只是告诉编译器按该类型理解它，不会自动检查 JSON。服务端返回 null、旧字段或错误数字时，浏览器仍会拿到这些真实值。

在数据边界先把外部值视为未知，检查对象结构、必需字段、字段类型与允许范围，再转换成内部记录。资料 ID 应是非空字符串，标题有明确长度规则；日期要约定格式与时区，不能只检查“它是字符串”。验证失败时提供明确错误，而不是用空标题掩盖契约问题。

类型检查与运行时验证可以共享契约设计，但不能互相冒充。编译通过说明静态关系满足当前编译器检查；合成响应通过验证说明这份输入符合检查条件。两者都不能证明真实服务永远不会返回新的或无效数据。

## 用状态标签减少不可能组合

多个布尔值可能同时出现 loading=true、hasError=true、hasData=true，迫使每个渲染分支猜优先级。带 kind 标签的联合状态可以明确每个分支有哪些字段：loading 不含结果，success 有列表，error 有错误信息。

如果产品需要边加载边展示旧结果，就把 refreshing 设计成独立状态并定义保留哪些数据，而不是偷偷违反 loading 的约束。类型应表达实际产品行为，不能为了简化代码取消用户需要的反馈。

TypeScript 可以根据标签缩小联合类型范围，帮助在不同分支访问正确字段。新增状态时检查所有处理分支，避免遗漏。错误本身也要区分可重试的连接失败和不可重试的契约错误，不把所有失败都压成一个无意义字符串。

## 组件输入与修改责任要明确

资料卡片接收记录、是否已收藏和操作状态，展示这些值，并通过事件或回调告诉上层用户想收藏哪条资料。持有列表的上层决定更新与持久化。多个组件直接改同一份可变全局对象，会让数据从哪里变化变得难以追踪。

显示的过滤结果、记录数量和按钮文案通常可以从现有状态推导。再保存一份独立数量就需要每次同步，容易留下旧值。为外部系统同步使用合适的副作用机制；纯计算不应每次都用副作用把结果写回状态。

组件划分按变化责任，而不是每个 HTML 元素都建一个文件。搜索输入、请求结果、单条卡片可以有清楚边界；只有一个属性的包装组件若没有复用或规则价值，可能只是增加跳转成本。先让接口清楚，再决定目录数量。

## React 与 Vue 的状态机制各有约束

React 渲染时读取的 state 是本次渲染的快照，setter 安排后续更新，不会把当前函数已经读取的值立刻改成新值。组件在树中的位置与 key 影响身份和状态保留。列表使用稳定资料 ID，避免插入或排序后把某张卡片的局部状态错配给另一张。

Vue 提供响应式依赖与组件更新机制，状态变化会通过框架调度触发相关更新。也需要遵守其响应式用法和组件输入约定。不能因为对象能变化，就让任何模块随意修改它；响应式追踪不自动决定业务所有权。

选择框架时比较团队经验、生态依赖、现有项目和交付要求，不凭几句语法宣布谁永远更快。迁移框架会改变构建与组件方式，但不会替你修好接口权限、状态契约或重复数据来源。先定位维护问题，再判断迁移是否必要。

## 离线检查每种状态允许的字段

函数按 kind 检查合成状态：成功必须有列表，错误必须有字符串，加载不能携带结果或错误。断言包含一个非法组合，明确显示契约能拒绝什么。它不是 TypeScript 编译器，也不模拟 React 或 Vue 的更新调度。

```python
def valid(state):
    kind = state.get("kind")
    if kind == "loading":
        return "data" not in state and "error" not in state
    if kind == "success":
        return isinstance(state.get("data"), list) and "error" not in state
    if kind == "error":
        return isinstance(state.get("error"), str) and "data" not in state
    return False
assert valid({"kind": "success", "data": [1]})
assert not valid({"kind": "loading", "data": [1]})
print(valid({"kind": "error", "error": "offline"}))
```

输出为 `True`，表示 `{'kind': 'error', 'error': 'offline'}` 满足这个简化规则。成功列表中的元素尚未校验，空错误字符串也会被接受；如果业务不允许，必须加上相应规则。通过检查不等于完整资料对象已可信。

模型将加载与旧结果分离，因此不能直接用它表达 refreshing。扩展时先定义该状态字段和转换条件，再增加断言。不要为一个实际需要的新状态直接放松所有分支，否则原有约束就失去作用。

组件错误边界、请求错误和字段验证失败也需要分开处理。界面渲染异常可能需要备用视图；请求失败需要保留查询与重试；契约失败需要记录安全的字段信息并停止把数据当成功结果使用。把这三种情况都捕获后返回空列表，会让维护者失去定位依据，也让用户误以为确实没有资料。

新增字段时也要保留旧响应测试，明确兼容边界，而不是只更新类型声明。

## 练习：从契约到组件验收

为资料定义 ID、标题、分类和下载状态。写出外部 JSON 验证规则、内部状态标签，以及卡片输入和收藏事件。给五份合成响应：正常、缺标题、错误类型、空列表、未知状态。每份都记录是否接受及界面反馈。

验收要求：错误数据不会以成功记录进入组件；加载、成功与错误不能组成非法混合状态；筛选后的数量由记录推导；插入和重新排序不把卡片状态交给别的 ID。若使用框架，应在真实项目验证更新与身份行为，并说明实际运行的检查；离线状态函数只证明自己的契约。

## 资料来源与延伸阅读

- [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [Managing React state](https://react.dev/learn/managing-state)
- [Vue essentials and components](https://vuejs.org/guide/introduction.html)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [同一份数据为什么会被读成不同的值](https://yishto.com/learn/computing/encoding-and-types/)
- [用 React Native 与 Expo 设计离线笔记](https://yishto.com/learn/application-platforms/expo-offline-notebook/)
