Skip to content

TypeScript 核心(三) ​

本页对应 TypeScript 核心,共 6 段视频,约 54 分钟。

学习目标 ​

  1. 能说明 TypeScript 是什么、与 JavaScript 的关系,并列举它在开发期类型检查、错误提前暴露、编辑器智能提示和代码可维护性方面带来的好处。
  2. 能使用 as 语法对 getElementById 等 API 返回的宽泛类型进行类型断言,获取更精确的元素类型并访问其特有属性;能说明断言只影响编译期校验、无法避免运行时错误,并举例说明滥用断言导致的运行时报错。
  3. 能识别接口或类型中重复的结构,将可变部分抽象为泛型参数,分别用泛型接口和泛型 type 重写同一数据结构,并在使用时正确传入具体类型参数。
  4. 能编写泛型函数(如 createArray),在函数名后声明类型参数 T,在调用时显式传入具体类型,使参数与返回值的类型随实参类型变化,并能验证类型不匹配时的报错。
  5. 能使用 extends 为泛型添加约束(如约束为含 length: number 的接口),使函数内部可安全访问约束类型上的属性,并能说明约束前后调用校验的差异。
  6. 能按"先主线后细节"的思路完成页面刷新次数记录小案例,为本地存储数据结构、时间格式化函数、渲染函数编写类型注解,并用类型断言处理 DOM 元素类型后完成渲染。

知识点 ​

本节把类型断言、泛型和综合案例串起来。以下代码依据转写稿整理,重点看“为什么需要→怎么想→怎么做→易错点”。

类型断言:开发者比 TS 更清楚类型时 ​

为什么需要:有些 DOM API 的返回值类型比较宽泛。比如 getElementById 推断出来是 HTMLElement,它身上没有 href,直接访问会报类型错误;但开发者明确知道这个 id 对应的是 <a> 元素。

怎么想:TS 不知道 link 这个 id 放在哪个元素上,所以只能推断成宽泛类型。此时用断言告诉 TS:“我确定它是 HTMLAnchorElement。”

怎么做:

ts
const link = document.getElementById('link') as HTMLAnchorElement;
console.log(link.href);

as 是类型断言的固定语法。断言后,link 就能访问 href,类型校验通过。

易错点:断言只能欺骗 TS 编译器,不能避免运行时错误。转写稿里的例子是:函数参数是 string | number,内部断言成 number 后调用 toFixed。传入数字 100 正常;传入字符串 '100' 也能通过类型校验,但运行时会报错,因为字符串没有 toFixed。所以除非特别确定类型,否则不要滥用断言。

泛型:把可变类型抽成参数 ​

为什么需要:两个数据结构大部分字段相同,只有 data 部分不同。如果重复定义两次接口,会产生大量重复代码。

怎么想:用“两步走”。第一步,找到可变部分,用泛型参数 T 占位;第二步,使用时把具体类型传给 T。

怎么做:

ts
interface IUser {
  name: string;
  age: number;
}

interface IGoods {
  id: number;
  goodsName: string;
}

interface IResData<T> {
  code: number;
  message: string;
  data: T;
}

const userRes: IResData<IUser> = {
  code: 200,
  message: 'success',
  data: { name: '张三', age: 18 }
};

const goodsRes: IResData<IGoods> = {
  code: 200,
  message: 'success',
  data: { id: 1101, goodsName: '衬衫' }
};

IResData<T> 把公共的 code、message 固定下来,把可变的 data 交给 T。使用时传 IUser 或 IGoods,就能限制 data 的结构。

易错点:泛型参数要放在真正可变的位置;使用时必须传具体类型,否则 T 只是占位,无法约束数据。

泛型 type 与泛型函数 ​

为什么需要:泛型不只接口能用,type 和函数也能用。函数中某些参数类型不固定时,泛型可以复用同一套逻辑。

怎么想:type 的写法和接口相似,在类型名后加 <T>;函数则在函数名后加 <T>,让参数、返回值共享这个类型。

怎么做:

ts
type ResData<T> = {
  code: number;
  message: string;
  data: T;
};

function createArray<T>(len: number, value: T): T[] {
  const arr: T[] = [];
  for (let i = 0; i < len; i++) {
    arr[i] = value;
  }
  return arr;
}

const nums = createArray<number>(4, 100);
const strs = createArray<string>(4, '100');

createArray<number> 会限制 value 必须是 number;传字符串就会报类型错误。

易错点:调用时传错具体类型,TS 会立即提示;不要为了“省事”把 T 写成 any,否则失去泛型的约束意义。

泛型约束:既灵活又限定范围 ​

为什么需要:泛型内部如果访问 length,TS 不知道 T 到底有没有 length,会报错。因为不是所有类型都有 length。

怎么想:定义一个包含 length 的接口,用 T extends HasLength 约束 T 必须满足这个范围。

怎么做:

ts
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(obj: T) {
  console.log(obj.length);
}

logLength({ length: 10 });
logLength([1, 2, 3]);
// logLength(100); // 报错:number 没有 length

易错点:约束不是把 T 变成固定类型,而是限定“必须满足某结构”。对象、数组都可以传,只要满足 length: number。

综合案例:先主线后细节 ​

为什么需要:刷新次数和时间记录涉及读取本地存储、取最后一条、次数加一、渲染页面、再存回本地,步骤多,直接写容易乱。

怎么想:先写主函数 updateList,把核心步骤列出来;每个细节先写成小函数占位,再逐个实现、逐个测试。这是“先主线、后细节”的自顶向下思路。

怎么做:核心结构如下:

ts
interface RecordItem {
  count: number;
  time: string;
}

function updateList() {
  const list = getList();
  const last = list[list.length - 1];
  const count = last ? last.count + 1 : 1;

  list.push({ count, time: getFormatTime() });
  render(list);
  setList(list);
}

getList 读取本地列表,注意 localStorage.getItem 可能返回 null,可用 JSON.parse(localStorage.getItem(KEY) || '[]') 兼容;render 里 getElementById('app') 可能为 null,明确是 div 时可用 as HTMLDivElement 断言;setList 用 JSON.stringify 存回本地。

易错点:第一次刷新时取不到最后一条记录,count 要初始化为 1;本地存储取不到时要给空数组默认值;断言 DOM 元素前要确认元素确实存在,否则运行时仍可能出错。

先把定义拆开:TypeScript 是具有类型语法的 JavaScript ​

本节笔记只有一句核心定义:TypeScript 是具有类型语法的 JavaScript,是一门强类型的编程语言。这句话需要拆成两层理解。

第一层,它仍然是 JavaScript。TypeScript 不是另起炉灶的运行时语言,它最终要落回 JavaScript 执行;你写的逻辑、函数、对象、DOM 操作,仍然是 JavaScript 那一套。第二层,它在 JavaScript 之上增加了“类型语法”,例如类型注解、接口、泛型、类型断言等,用来描述数据长什么样、函数接收什么、返回什么。

TypeScript 与 JavaScript 关系示意图

为什么需要先建立这个认知?因为初学者容易把 TypeScript 当成“另一门语言”,从而把类型和运行逻辑混在一起。更合理的想法是:类型是开发阶段的约束和说明,运行阶段仍然是 JavaScript。所以学习 TS 时,先写清楚 JS 逻辑,再补类型描述;不要为了类型而绕乱逻辑。易错点是:以为写了类型,运行时就不会错——这并不成立。

为什么需要:把错误从运行时提前到开发时 ​

JavaScript 是动态类型语言,很多错误只有运行到那一行才会暴露。TypeScript 的价值,是在开发阶段通过类型检查提前发现“这个值可能没有这个属性”“这个参数类型不匹配”等问题。

怎么想?把类型检查看成开发期的“预警系统”,而不是运行时的“保险箱”。怎么做?写代码时优先让 TS 自动推断;推断不够精确时,再补类型注解、接口、泛型或断言。易错点是:把所有变量都写成 any,等于放弃类型检查;或者把类型断言当成类型转换,后面会看到这会带来运行时风险。

怎么想:类型语法是在描述“数据形状”和“约束关系” ​

“具有类型语法的 JavaScript”可以理解为:JS 负责“做什么”,类型语法负责“允许什么”。例如对象有哪些字段、字段是什么类型、函数参数能否是字符串或数字、数组里装什么。类型语法不是装饰,它让编辑器能提示、让编译器能校验、让团队能读懂数据结构。

怎么做?先抓住三条线:一是类型推断,能推断就不手写;二是类型注解,推断不清时明确写出;三是类型复用,多个结构相似时用接口、类型别名、泛型抽象可变部分。易错点是:类型写得过宽,失去检查意义;类型写得过死,导致复用困难。

怎么做:从转写稿的调试片段看“推断—断言—验证” ​

转写稿中有一个典型调试过程,可以帮助理解 TS 的类型检查边界。先看不断言的写法:

ts
const link1 = document.getElementById('link')
link1.href // 报错:HTMLElement 上不存在 href

推导过程是:getElementById 只能知道拿到了一个元素,TS 自动推断为较宽泛的 HTMLElement;它不知道这个 id 一定放在 <a> 上,所以访问 href 会报类型错误。开发者比 TS 更清楚它是 A 元素,于是使用类型断言:

ts
const link2 = document.getElementById('link') as HTMLAnchorElement
link2.href // 通过类型校验

这里 as HTMLAnchorElement 把类型断言得更精确,href 就能通过校验。但转写稿紧接着用另一个例子提醒:断言只影响 TS 编译器,不保证运行时正确。

ts
function format(foo: string | number) {
  console.log((foo as number).toFixed(2))
}

format(100)   // 正常
format('100') // 运行时错误:foo.toFixed is not a function

推导过程是:参数 foo 允许 string | number,传字符串 '100' 能通过类型校验;函数内部却断言成 number 并调用 toFixed,运行时它实际是字符串,没有这个方法,于是报错。结论是:断言只能“告诉编译器我知道类型”,不能“把值真的转换成那个类型”。

易错点:TS 是强类型检查,但不是运行时保险 ​

学习“什么是 TS”后,要记住几个边界:第一,TS 的类型语法最终会被擦除,运行的是 JavaScript;第二,类型断言不是类型转换,滥用会把错误推迟到运行时;第三,强类型不等于“所有值都固定不变”,而是开发阶段有约束、有检查;第四,不要用 any 逃避问题,否则类型系统形同虚设。

所以,本节最需要建立的判断是:TypeScript = JavaScript 逻辑 + 类型语法约束 + 开发期检查。先理解这一点,后续再学类型断言、泛型、泛型约束时,就不会把它们当成孤立的语法,而会知道它们都在回答同一个问题:如何用类型更准确地描述代码,同时又不破坏 JavaScript 的运行本质。

本节要解决的问题 ​

这一节笔记只讲 TypeScript 带来的两个直接好处:静态类型检查,提前发现代码错误;良好的代码提示,提升开发效率。不要把它理解成“TS 比 JS 高级”这种空话,而要从日常开发场景看:JS 里很多错误要等页面运行、点到某个按钮、走到某行代码才暴露;TS 则通过类型注解、类型推断和编辑器语言服务,把一部分问题提前到写代码时暴露。转写稿后面的类型断言、泛型、综合案例,其实都在反复演示这两个好处:鼠标移入能看到推断结果,类型不匹配会出现红色波浪线,类型精确后点号后面会出现补全。

好处一:静态类型检查,提前发现代码错误 ​

为什么需要: JS 里变量、函数参数、对象字段都没有强制约束。比如把字符串当数字用、对象字段名拼错、函数返回值当数组用,往往要运行时才报错。TS 的类型检查发生在编译阶段和编辑器语言服务中,能在保存前、运行前给出错误提示。

怎么想: 把类型看成“约束”。变量、参数、返回值、对象结构先声明约束,编译器再根据赋值和调用检查是否匹配;如果类型太宽泛,检查能发现“这个宽类型上不存在某属性”;如果类型写错,调用处会直接报错。转写稿中 getElementById('link') 被推断为较宽泛的 HTMLElement,此时访问 href 会报错,因为 TS 不知道这个 id 一定放在 A 元素上。开发者比 TS 更清楚时,才用 as HTMLAnchorElement 断言成更精确的类型。

静态类型检查提前发现错误示意图

怎么做: 先写类型注解,再让编辑器检查。转写稿里的关键调试过程可以简化为:

ts
const link1 = document.getElementById('link')
// 转写稿中鼠标移入:link1 被推断为 HTMLElement
// link1.href 会报错:HTMLElement 上不存在 href

const link2 = document.getElementById('link') as HTMLAnchorElement
link2.href // 通过校验,并且能获得 href 提示

泛型函数也能体现静态检查。转写稿中 createArray<T> 的第二个参数由 T 决定:

ts
function createArray<T>(len: number, value: T): T[] {
  const arr: T[] = []
  for (let i = 0; i < len; i++) arr[i] = value
  return arr
}

createArray<number>(4, 100) // 通过
createArray<number>(4, '100') // 报错:string 不能赋给 number

综合案例里也有类似调试:localStorage.getItem 得到的是字符串或 null,不能直接当数组返回,需要 JSON.parse(raw || '[]');getElementById('app') 得到 HTMLElement | null,不能直接访问 innerHTML,转写稿中通过断言为 HTMLDivElement 后解决。

静态类型检查报错示意图

易错点: 类型断言只能“欺骗 TS 编译器”,不能避免运行时错误。转写稿的 foo(value: string | number) 中把 value 断言成 number 再调 toFixed,传 '100' 时类型校验能过,但运行时字符串没有 toFixed,仍会报错。所以不要滥用 as;any 也会让检查失效;getElementById 这类 API 还要处理 null。

好处二:良好的代码提示,提升开发效率 ​

为什么需要: 开发时不可能记住所有 DOM API、对象字段和函数参数。没有类型时,编辑器只能给很弱的提示;有类型后,点号后面能列出可用属性和方法,参数写错也能立刻看到红线。

怎么想: 代码提示不是额外插件,而是类型信息在编辑器里的表现。类型越精确,提示越准确;类型越宽泛或写成 any,提示就越少。转写稿中 link2 断言为 HTMLAnchorElement 后,再写 link2. 会出现 href 等联想,选中后类型校验通过;而 link1 是宽泛的 HTMLElement,访问 href 直接报错。

代码提示提升开发效率示意图

怎么做: 给对象定义 interface 或 type,给函数写参数和返回值类型,给泛型加约束。比如泛型约束 T extends { length: number } 后,函数内部访问 obj.length 才不报错;调用时传对象、数组可以,传 number 不行。转写稿中 logLength<T extends LengthObj>(obj: T) 的调试过程就是:不加约束时 obj.length 报“T 不存在 length”;加上约束后通过,并且传数组、带 length 的对象都能获得提示。

ts
interface LengthObj {
  length: number
}
function logLength<T extends LengthObj>(obj: T) {
  console.log(obj.length)
}
logLength({ length: 10 }) // 通过
logLength([1, 2, 3]) // 通过
// logLength(100) // 报错:number 没有 length

易错点: 不要为了省事全写 any,否则提示会退化;接口字段名拼错时,提示和红线能帮你发现,但前提是类型写对;类型过宽时提示少,需要收窄类型或使用泛型约束;断言后虽然提示变多,但要确保运行时对象真的符合该类型。

两个好处如何配合 ​

静态检查负责“早发现错误”,代码提示负责“少查文档、少写错”。二者都依赖同一件事:让类型信息尽量准确。类型注解、类型推断、泛型、类型断言、泛型约束,都是围绕这个目标服务的。使用时记住转写稿反复强调的边界:断言和类型检查不能替代运行时校验;泛型能复用类型,但要用约束限制“不能随便传”;遇到复杂需求先主线后细节,先让类型结构清楚,再逐步补实现。

课件图示 ​

课件第56页:类型断言的基本使用 作用:有些时候开发者比TS本身更清楚当前的类型是什么,可以使

课件第63页:泛型约束 作用:泛型的特点就是灵活不确定,有些时候泛型函数的内部需要访问一些特定

课件第64页:总结 总结 1. 泛型的好处是什么? 增加类型的复用性和灵活性 2. 泛型实现的

课件第6页:怎么学 TS核心 + 综合案例 Vue3 + TS + 实战案例 TS业务中常用

课件第57页:类型断言的注意事项 类型断言只能够「欺骗」TypeScript 编译器,无法避免

课件第8页:为什么需要编译环境? TypeScript编写的代码是无法直接在js引擎(浏览器

课件第13页:TS类型注解是什么 概念:类型注解指的是给变量添加类型约束,使变量只能被赋值为约

课件第19页:如何注解数组类型 使用数据类型对变量进行类型注解有俩种语法 语法一(推荐): 说

代码示例 ​

示例一:类型断言(as)与断言的坑 ​

ts
// ---------- 1. DOM 类型断言:让类型更精确 ----------
// 不加断言:getElementById 返回较宽泛的 HTMLElement,访问 href 会报错
const link1 = document.getElementById('link')
// link1.href  // ❌ 类型“HTMLElement”上不存在属性“href”

// 加断言:我(开发者)比 TS 更清楚它是 A 元素
const link2 = document.getElementById('link') as HTMLAnchorElement
console.log(link2.href) // ✅ 有类型提示,类型校验通过

// ---------- 2. 断言只能欺骗编译器,不能避免运行时错误 ----------
function format(foo: string | number) {
  // 我明确知道它是 number,所以断言后调用 number 专有方法
  console.log((foo as number).toFixed(2))
}

format(100)     // ✅ 输出 100.00
format('100')   // ❌ 类型校验通过,但运行时报错:
                //    foo.toFixed is not a function

要点解释

  • as XXX 是断言语法,作用是在 TS 自动推断“不够精确”时,把它断言成更具体的类型,从而获得该类型上的属性和方法提示。
  • 断言的前提是:开发者比 TS 更清楚当前类型,否则不要用。
  • 易错点一:getElementById 在严格模式下返回 HTMLElement | null,直接 .href 前建议判空(if (link2) ...),否则 null 也会报错。
  • 易错点二(重点):断言只影响编译期,编译时放过、运行时不保证。format('100') 满足 string | number 的联合类型,能通过校验,但字符串没有 toFixed,运行时直接崩。断言不可滥用。

示例二:泛型接口 / 泛型 type / 泛型函数 / 泛型约束 ​

ts
// ---------- 1. 可变部分抽象成泛型参数 T ----------
interface IResData<T> {
  code: number
  message: string
  data: T          // 可变部分用 T 占位
}

interface User {
  name: string
  age: number
}
interface Goods {
  id: number
  goodsName: string
}

// 使用时把具体类型传进去,data 就被这个具体类型约束住了
const userRes: IResData<User> = {
  code: 200,
  message: 'success',
  data: { name: '张三', age: 18 }
}

const goodsRes: IResData<Goods> = {
  code: 200,
  message: 'success',
  data: { id: 1101, goodsName: '手机' }
}

// ---------- 2. 泛型 type:把 interface 换成 type 即可 ----------
type ResData<T> = {
  code: number
  message: string
  data: T
}

const userRes2: ResData<User> = {
  code: 200,
  message: 'success',
  data: { name: '李四', age: 20 }
}

// ---------- 3. 泛型函数:T 在参数、返回值、函数体里共享 ----------
function createArray<T>(len: number, value: T): T[] {
  const arr: T[] = []
  for (let i = 0; i < len; i++) {
    arr[i] = value
  }
  return arr
}

createArray<number>(4, 100)    // ✅ [100, 100, 100, 100]
createArray<string>(4, '100')  // ✅ ['100', '100', '100', '100']
// createArray<number>(4, '100') // ❌ 类型“string”不能赋给“number”

// ---------- 4. 泛型约束:既保留灵活,又限制必须有的属性 ----------
interface LengthObj {
  length: number
}

function logLen<T extends LengthObj>(obj: T) {
  console.log(obj.length) // 不加约束时:类型“T”上不存在属性“length”
}

logLen({ length: 10 })  // ✅ 手动写了 length
logLen([1, 2, 3])       // ✅ 数组自带 length
// logLen(100)          // ❌ number 没有 length,不通过校验

要点解释

  • 泛型的通用套路是“两步走”:① 找到类型不确定的部分,抽象成泛型参数 T(写在接口/类型名/函数名后的 <> 里);② 使用时把具体类型传进 <>,用它去限制那个可变位置。
  • IResData<T> 中 code、message 是公共部分只写一次,只有 data 用 T 占位,从而避免重复定义两个几乎相同的接口——这就是泛型“复用 + 灵活”的价值。
  • 易错点一:在对象注解场景下 type 几乎可以替换 interface,改造时只需把 interface X { 换成 type X = {,注意补上等号。
  • 易错点二:泛型函数里如果访问了 T 身上不存在的属性(如 length),一定报错;解决方式是 T extends LengthObj 泛型约束,extends 在这里是“约束/继承”,不是普通类继承。
  • 易错点三:调用时显式写 createArray<number> 后,第二个参数就必须是 number,传字符串会立刻飘红。

示例三:综合小案例——记录页面刷新次数与时间 ​

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>刷新次数记录</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- 编译后的 JS 放这里;在 Vite / ts 环境中直接写 src="./main.ts" -->
    <script src="./main.js"></script>
  </body>
</html>
ts
// main.ts
interface ListItem {
  count: number
  time: string
}

const TS_KEY = 'TS_LIST_KEY'

// 1. 取本地列表
function getList(): ListItem[] {
  const str = localStorage.getItem(TS_KEY)
  // getItem 可能返回 null,先兜底成 '[]' 再 parse
  return JSON.parse(str ?? '[]')
}

// 2. 存本地列表
function setList(list: ListItem[]) {
  localStorage.setItem(TS_KEY, JSON.stringify(list))
}

// 3. 格式化当前时间为 h:m:s
function getFormatTime(): string {
  const date = new Date()
  const h = date.getHours()
  const m = date.getMinutes()
  const s = date.getSeconds()
  return `${h}:${m}:${s}`
}

// 4. 渲染到页面
function render(list: ListItem[]) {
  const app = document.getElementById('app') as HTMLDivElement
  const strArr = list.map((item) => {
    return `刷新次数为${item.count},刷新时间为${item.time}`
  })
  app.innerHTML = strArr.join('<br>')
}

// 5. 主流程:取列表 → 末项 +1 → 渲染 → 存回本地
function updateList() {
  const list = getList()
  const lastItem = list[list.length - 1]

  list.push({
    count: lastItem ? lastItem.count + 1 : 1, // 第一次刷新时没有末项,初始化为 1
    time: getFormatTime()
  })

  render(list)
  setList(list)
}

updateList()

要点解释

  • 整体是“先主线、后细节”的自顶向下写法:updateList 里只保留四个核心步骤(取列表 → 末项 +1 → 渲染 → 存本地),每个步骤拆成独立小函数,单点实现、单点测试。
  • getList 的返回类型必须注解为 ListItem[],因为 localStorage.getItem 拿到的永远是字符串,需要 JSON.parse 还原成数组结构。
  • 易错点一:getItem 可能返回 null,直接 JSON.parse(null) 会出问题,所以要 ?? '[]' 兜底——外层字符串是给 JSON.parse 用的,内层空数组才是给返回值类型用的。
  • 易错点二:getElementById('app') 的类型是 HTMLElement | null,没有 innerHTML 的精确类型,需要用 as HTMLDivElement 断言(此处断言是安全的,因为 HTML 里确实有 id="app")。
  • 易错点三:setList 存进去必须 JSON.stringify,直接存数组会变成 [object Object]。
  • 易错点四:count 的三元判断不能省,第一次刷新时 list 为空,lastItem 是 undefined,直接 lastItem.count + 1 会运行时报错。

常见错误 ​

  1. document.getElementById('link').href 报红

    • 现象:Property 'href' does not exist on type 'HTMLElement',但浏览器里跑起来没事。
    • 原因:getElementById 的返回类型被推断成宽泛的 HTMLElement | null,TS 不知道这个 id 挂在 <a> 上;而且 null 分支也没处理。
    • 修法:const link = document.getElementById('link') as HTMLAnchorElement(或用 if (!link) return 先排除 null,再断言);断言只做一次、集中在取值处,别在后续每一行反复 as。
  2. 联合类型里断言成 number 再调 toFixed,编译通过但运行崩

    • 现象:(fo as number).toFixed(2) 类型校验全绿,传入 '100' 时控制台报 fo.toFixed is not a function。
    • 原因:断言只是让编译器闭嘴,不改变运行时的实际值;调用方按联合类型合法地传了字符串。
    • 修法:改用类型收窄 if (typeof fo === 'number') { ... },或函数重载 / 运行时校验后 throw;断言只在“我确实比编译器知道得多”时使用。
  3. 泛型函数体里访问 obj.length 报错

    • 现象:Property 'length' does not exist on type 'T'。
    • 原因:T 无约束时可以是任何类型,编译器无法保证有 length。
    • 修法:定义 interface HasLength { length: number },写成 <T extends HasLength>;调用方传 number 时应当被拦下——如果没被拦下,说明约束写漏了。
  4. JSON.parse(localStorage.getItem(KEY)) 报参数类型错误

    • 现象:Argument of type 'string | null' is not assignable to parameter of type 'string'。
    • 原因:getItem 在键不存在时返回 null,首次访问页面必然命中这条路径。
    • 修法:JSON.parse(localStorage.getItem(KEY) ?? '[]'),并把结果断言/校验为 Item[];同时让 getList 的返回类型与 setList 的入参类型共用同一个 Item,避免两处各写一份。
  5. getElementById('app').innerHTML = ... 报“可能为 null”或类型不存在

    • 现象:Object is possibly 'null' / Property 'innerHTML' does not exist on type 'HTMLElement'。
    • 原因:同第 1 条,null 未处理 + 类型过宽;渲染函数里往往还叠加了 string[] 直接赋值给 innerHTML。
    • 修法:const app = document.getElementById('app') as HTMLDivElement,赋值前 join('<br>') 把数组转成字符串。
  6. 泛型接口“抽象了一半”:可变部分仍写死

    • 现象:ApiResponse 定义了两个版本,字段 code/message 完全重复;后来改 message 类型只改了一处,另一个接口静默不一致。
    • 原因:只把外层结构抽出来,没有把真正变化的那一段(data)参数化。
    • 修法:interface ApiRes<T> { code: number; message: string; data: T },使用时 ApiRes<User>、ApiRes<Goods>;判断标准是“除了某一段之外全都一样”就该上泛型。

练习 ​

  1. 封装 function getEl<T extends HTMLElement>(selector: string): T,内部用 querySelector + 断言实现。然后回答:为什么调用方写成 getEl<HTMLAnchorElement>('#link') 之后就能直接 .href?如果把泛型参数省略掉会发生什么?

  2. 实现 function pluck<T, K extends keyof T>(obj: T, key: K): T[K],要求 pluck({name:'a', age:18}, 'age') 的返回值被推断为 number,而传入不存在的键 'xxx' 时编译报错。写出签名并说明 K extends keyof T 在这里替代了哪种常见错误。

  3. 把综合案例里的 getList / setList 重构成 function createStorage<T>(key: string),返回 { get(): T[]; set(list: T[]): void }。要求:首次调用 get() 返回 [] 而不是报错;set() 之后 get() 能取回同样结构的数据。

  4. 改写视频 1 中的 toFixed 案例,用 typeof 收窄让函数在传入字符串时编译期就报错(或运行时给出明确提示),而不是留到运行时报 is not a function。对比说明“断言”和“收窄”在失败时机上的差别。

  5. 用泛型约束实现 function getById<T extends { id: number }>(list: T[], id: number): T | undefined,然后尝试用 getById(goodsList, 1) 和一个没有 id 字段的数组分别调用,记录编译器分别给出什么反馈。

原视频对照 ​

#视频时长
1TS核心-类型断言07:22
2TS核心-泛型概念理解和泛型接口10:47
3TS核心-泛型type和泛型函数09:37
4TS核心-泛型约束05:46
5TS综合小案例-0108:42
6TS综合小案例-0211:55

小结 ​

  • 类型断言的适用边界很窄:只有当你掌握编译器不掌握的信息时才用,且断言点越靠近数据来源越好。
  • 断言能改变编译结果,改变不了运行时;凡是有外部输入(用户传参、localStorage、DOM 查询)参与的地方,优先收窄与校验,其次才是断言。
  • 泛型的判断信号是“结构大部分相同、只有一小块不同”——把那一小块抽成参数,而不是复制一份类型。
  • 泛型接口、泛型 type、泛型函数三者写法一致,差异只在关键字;interface 与 type 在对象注解场景下基本可互换。
  • extends 在泛型里有两个身份:约束(T extends HasLength)和收窄键范围(K extends keyof T),都是把“随便传”变成“有范围地传”。
  • 类型定义应当“一处定义、多处引用”,getList 的返回类型和 setList 的入参类型必须同源,否则序列化/反序列化处最容易埋雷。
  • 复杂需求按“先主线、后细节”推进:先把主流程和函数骨架摆出来,再逐个填坑并单独验证,能显著减少联调时的排查面。
  • 泛型、断言这些手段解决的是类型层面的问题;运行时正确性仍然依赖你自己的分支判断(null、空数组、首次访问)。