H5 TypeScript 领域类型设计

举报
shenlan9755 发表于 2026/09/17 09:05:55 2026/09/17
【摘要】 H5 TypeScript 领域类型设计TypeScript 的价值不仅是给变量补充类型标注,更重要的是把业务约束放进类型模型,让错误状态难以被构造。若所有接口都使用宽泛对象、字符串和可选字段,编译器无法帮助识别状态冲突,代码最终仍要依赖大量运行时判断。 一、避免用 string 表达所有概念订单编号、用户编号和商品编号在运行时都是字符串,但业务含义完全不同。可以使用品牌类型减少误传:ty...

H5 TypeScript 领域类型设计

TypeScript 的价值不仅是给变量补充类型标注,更重要的是把业务约束放进类型模型,让错误状态难以被构造。若所有接口都使用宽泛对象、字符串和可选字段,编译器无法帮助识别状态冲突,代码最终仍要依赖大量运行时判断。

一、避免用 string 表达所有概念

订单编号、用户编号和商品编号在运行时都是字符串,但业务含义完全不同。可以使用品牌类型减少误传:

type Brand<T, Name extends string> = T & {
  readonly __brand: Name;
};

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function loadOrder(orderId: OrderId): Promise<Order> {
  return orderRepository.load(orderId);
}

品牌类型只在编译阶段提供区分。外部字符串不能直接断言为业务标识,应先经过格式或来源校验,再由统一函数构造。

二、用联合类型表达互斥状态

type PaymentFailure =
  | { type: 'balanceInsufficient' }
  | { type: 'permissionDenied' }
  | { type: 'temporarilyUnavailable' };

type PaymentState =
  | { status: 'idle' }
  | { status: 'submitting'; requestId: string }
  | { status: 'succeeded'; receipt: Receipt }
  | { status: 'failed'; reason: PaymentFailure };

这比 isLoading、isSuccess、error 三个独立字段更安全,因为不会出现加载中同时成功的矛盾组合。

渲染时编译器会根据判别字段缩小类型:

function renderPayment(state: PaymentState): string {
  switch (state.status) {
    case 'idle':
      return '等待提交';
    case 'submitting':
      return '正在提交';
    case 'succeeded':
      return `已完成:${state.receipt.displayNumber}`;
    case 'failed':
      return getFailureMessage(state.reason);
  }
}

三、使用 never 做穷尽检查

业务状态新增分支时,希望编译器提示所有未更新位置:

function assertNever(value: never): never {
  throw new Error(`未处理状态:${String(value)}`);
}

function getFailureMessage(failure: PaymentFailure): string {
  switch (failure.type) {
    case 'balanceInsufficient':
      return '余额不足';
    case 'permissionDenied':
      return '当前账户无法执行此操作';
    case 'temporarilyUnavailable':
      return '服务暂时不可用';
    default:
      return assertNever(failure);
  }
}

运行时错误只是最后防线,主要价值是编译阶段要求开发者补全新分支。

四、外部数据从 unknown 开始

类型断言不会验证真实数据。接口响应、本地存储和跨窗口消息都属于外部输入,应先视为 unknown。

type UserSummary = {
  id: UserId;
  nickname: string;
};

function parseUserSummary(value: unknown): UserSummary {
  if (typeof value !== 'object' || value === null) {
    throw new Error('用户数据格式错误');
  }

  const record = value as Record<string, unknown>;
  if (typeof record.id !== 'string') {
    throw new Error('用户标识格式错误');
  }
  if (typeof record.nickname !== 'string') {
    throw new Error('用户昵称格式错误');
  }

  return {
    id: record.id as UserId,
    nickname: record.nickname
  };
}

实际项目应优先复用现有解析或校验组件。协议明确为必填的字段直接严格校验,不要把所有字段都做成可选来掩盖接口问题。

五、区分缺失、空值和空字符串

undefined、null 和空字符串可能代表不同业务含义。更新资料时,字段缺失可能表示“不修改”,null 可能表示“清除”,空字符串则可能是非法输入。

type UpdateProfileCommand = {
  nickname?: string;
  introduction?: string | null;
};

只有服务协议明确区分这些含义时才这样建模。前后端应对每种情况达成一致,不要在请求前随意把所有空值删除。

六、用映射类型表达变化

创建对象和更新对象通常具有不同必填规则。可以在稳定基础类型上构造输入类型,但不要追求过度抽象。

type Product = {
  id: string;
  name: string;
  priceInCent: number;
  enabled: boolean;
};

type ProductDraft = Omit<Product, 'id'>;
type ProductPatch = Partial<Pick<Product, 'name' | 'priceInCent' | 'enabled'>>;

ProductPatch 明确只能修改允许字段,避免把标识意外放进更新命令。

七、泛型应表达真实关系

泛型适合描述输入与输出之间的类型联系:

type Page<T> = {
  items: T[];
  page: number;
  hasMore: boolean;
};

interface Repository<Id, Entity> {
  load(id: Id): Promise<Entity>;
}

如果泛型参数只出现一次,或调用者必须传递多个无法理解的类型参数,抽象可能没有带来价值。业务代码的可读性优先于类型技巧。

八、只读类型减少意外修改

type ReadonlyCart = {
  readonly id: string;
  readonly items: readonly CartItem[];
};

只读是编译期约束,不会自动冻结运行时对象,但它能让状态更新入口更明确。配合不可变更新,可以避免组件直接修改共享数组。

九、不要滥用 any 和类型断言

any 会关闭类型检查,并沿调用链扩散。临时接入未知数据时优先使用 unknown,在边界处缩小类型。

类型断言只适用于开发者掌握了编译器无法推导的信息,不能替代运行时校验。连续进行多次断言通常意味着模型或边界设计需要调整。

十、让类型跟随业务语言

类型名、字段名和状态分支应使用团队共同理解的业务术语。不要把协议层缩写直接扩散到界面层,也不要为同一概念建立多个含义接近的类型。

重构时可以先从高风险状态开始:支付、权限、登录和异步流程最适合使用可辨识联合。小型纯展示对象则保持简单即可。

总结

TypeScript 领域建模的目标是让非法状态更难出现。通过品牌类型区分业务标识,用联合类型表达互斥状态,从 unknown 校验外部输入,并用只读与精确更新类型限制修改范围,编译器才能真正参与业务正确性保障。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。