1.4 TypeScript基础与类型驱动开发


文档摘要

1.4 TypeScript 基础与类型驱动开发 TypeScript 是 JavaScript 的类型化超集,Angular 从诞生起就以它为第一语言。本节聚焦四件武器——接口、泛型、联合类型与可空类型——并说明它们如何被 Angular 的模板类型检查消费:模板里绑定表达式的类型错误在编译期就能暴露,等于把变更检测要检查的"数据形状"提前钉死。 前两节建好了项目、看清了组件树。这一节给树上的数据流装护栏:类型越严,绑定表达式出错的空间越小。 先定位,再出发 在写第一段业务代码之前,先把 TypeScript 这门带类型安全网的语言放进工具箱;位置摆对了,后面每一章都省力: 用接口与类型别名建模业务对象,区分两者的适用场景。 编写泛型函数与泛型服务,理解类型参数的推断与显式指定。

1.4 TypeScript 基础与类型驱动开发

TypeScript 是 JavaScript 的类型化超集,Angular 从诞生起就以它为第一语言。本节聚焦四件武器——接口、泛型、联合类型与可空类型——并说明它们如何被 Angular 的模板类型检查消费:模板里绑定表达式的类型错误在编译期就能暴露,等于把变更检测要检查的"数据形状"提前钉死。

前两节建好了项目、看清了组件树。这一节给树上的数据流装护栏:类型越严,绑定表达式出错的空间越小。

先定位,再出发

在写第一段业务代码之前,先把 TypeScript 这门带类型安全网的语言放进工具箱;位置摆对了,后面每一章都省力:

  1. 用接口与类型别名建模业务对象,区分两者的适用场景。
  2. 编写泛型函数与泛型服务,理解类型参数的推断与显式指定。
  3. 用联合类型与类型收窄消除非法状态。
  4. 解释严格空检查如何与模板类型检查协作,在编译期拦截绑定错误。

一、接口与类型别名:给数据定形状

以教程贯穿的订单域为例,先给核心对象定形:

// 接口描述对象形状:只关心结构,不关心实现 interface Order { readonly orderNo: string; // 只读:创建后不允许改单号 amount: number; status: OrderStatus; items: OrderItem[]; // 条目列表,至少一条的约束见下文 } // 类型别名描述联合:状态只有四个合法值 type OrderStatus = '待支付' | '已付款' | '已发货' | '已完成'; interface OrderItem { sku: string; quantity: number; }

接口与类型别名的分工我个人的判断是:对象形状用接口(可被类实现、可声明合并),联合、交叉、工具类型用别名。团队统一即可,混用不报错但伤一致性。

readonly 值得多说一句:它不只是文档,配合严格模式,任何试图改写 orderNo 的代码在编译期报错。变更检测视角下,只读字段意味着"这个绑定永远不需要检查更新"——框架做检查时它天然是稳定输入。

二、泛型:让服务与工具函数跨类型复用

第 2 章的 HttpClient 会返回可观察对象,其泛型参数决定数据的类型形状。先从手写泛型体会:

// 泛型分页容器:后端接口几乎都长这样,写一次处处复用 interface Page<T> { items: T[]; // T 是类型参数,实例化时确定 total: number; page: number; pageSize: number; } // 泛型函数:从分页结果中安全抽取某一列 function pickColumn<K extends string, T extends Record<K, unknown>>( page: Page<T>, key: K ): T[K][] { return page.items.map(item => item[key]); // 返回类型由调用处推断 } // 使用:类型推断让调用处零注解 const orderPage: Page<Order> = { items: [ { orderNo: 'A-1', amount: 99, status: '已付款', items: [{ sku: 'X', quantity: 1 }] } ], total: 1, page: 1, pageSize: 20 }; const nos = pickColumn(orderPage, 'orderNo'); // 推断为 string[] // pickColumn(orderPage, 'nope') 编译期直接报错,键名拼错无处可逃

三、联合类型与类型收窄:把非法状态挡在编译期

业务代码大量 bug 来自"状态组合不合法"。用联合建模可以让非法状态不可表示:

// 用判别式联合替代散落的布尔标志 type PayState = | { phase: 'idle' } // 未开始 | { phase: 'processing'; startedAt: number } // 进行中才有时间戳 | { phase: 'failed'; reason: string } // 失败才有原因 | { phase: 'done'; paidAt: number; receiptNo: string }; // 完成才有回执 function renderHint(state: PayState): string { switch (state.phase) { // phase 是判别字段 case 'idle': return '等待支付'; case 'processing': return `支付中,开始于 ${new Date(state.startedAt).toLocaleTimeString()}`; case 'failed': return `支付失败:${state.reason}`; case 'done': return `已完成,回执号 ${state.receiptNo}`; } } // 若漏写任何一个分支,开 switch 穷尽检查的编译选项会报错

对比常见写法——一个 loading 布尔加一个 error 字符串——判别式联合从结构上禁止了"既在加载又已失败"这种矛盾状态。绑定到模板里,插值表达式直接调用 renderHint(state),变更检测每次检查拿到的都是类型保证过的合法数据。

四、严格空检查与模板类型检查

tsconfig 里的严格模式开启后,空值传播成编译错误。它的价值在模板里被放大:Angular 编译器会用组件成员的类型去检查模板里每个绑定表达式。做个错误示范:

@Component({ selector: 'app-order-brief', template: ` <!-- amount 是 number 类型,长度属性不存在,编译期报错 --> <p>金额:{{ order.amount.length }}</p> <!-- 正确写法:可空对象先收窄再取属性 --> <p>单号:{{ order?.orderNo }}</p> <p>状态:{{ order!.status }}</p> ` }) export class OrderBriefComponent { order?: Order; // 可选成员,模板里必须处理可能为空的情形 }

⚠️ 常见坑:! 非空断言是"我担保不为空"的契约,用错就是把空值错误从编译期推迟到运行时。模板里优先用 ?. 安全导航;只有确认赋值时序后(例如接口数据回来才渲染该区域)才用 !

图:类型系统与变更检测的分工

图:类型系统与变更检测的分工

本节要点回顾

  • 接口与别名分工:对象形状用接口,联合与工具类型用别名,团队统一即可。
  • 泛型的回报:分页容器、工具函数写一次跨域复用,类型推断让调用处零注解且拼错键名即报错。
  • 判别式联合:用 phase 字段建模状态机,非法状态不可表示,比布尔标志组合可靠得多。
  • 严格空检查?. 优先,! 是有代价的契约;模板类型检查把错误拦在构建阶段。
  • 主线上的一环:类型系统保证变更检测重算的每个表达式形状合法,检查只剩"值是否变化"一件事。

第 1 章到此收束。下一章进入正题:组件如何成为变更检测的执行单元,模块与注入体系如何决定服务的范围。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U