主题
TypeScript 核心(三)
本页对应 TypeScript 核心,共 6 段视频,约 54 分钟。
学习目标
- 能说明 TypeScript 是什么、与 JavaScript 的关系,并列举它在开发期类型检查、错误提前暴露、编辑器智能提示和代码可维护性方面带来的好处。
- 能使用
as语法对getElementById等 API 返回的宽泛类型进行类型断言,获取更精确的元素类型并访问其特有属性;能说明断言只影响编译期校验、无法避免运行时错误,并举例说明滥用断言导致的运行时报错。 - 能识别接口或类型中重复的结构,将可变部分抽象为泛型参数,分别用泛型接口和泛型
type重写同一数据结构,并在使用时正确传入具体类型参数。 - 能编写泛型函数(如
createArray),在函数名后声明类型参数T,在调用时显式传入具体类型,使参数与返回值的类型随实参类型变化,并能验证类型不匹配时的报错。 - 能使用
extends为泛型添加约束(如约束为含length: number的接口),使函数内部可安全访问约束类型上的属性,并能说明约束前后调用校验的差异。 - 能按"先主线后细节"的思路完成页面刷新次数记录小案例,为本地存储数据结构、时间格式化函数、渲染函数编写类型注解,并用类型断言处理 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。所以学习 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,否则提示会退化;接口字段名拼错时,提示和红线能帮你发现,但前提是类型写对;类型过宽时提示少,需要收窄类型或使用泛型约束;断言后虽然提示变多,但要确保运行时对象真的符合该类型。
两个好处如何配合
静态检查负责“早发现错误”,代码提示负责“少查文档、少写错”。二者都依赖同一件事:让类型信息尽量准确。类型注解、类型推断、泛型、类型断言、泛型约束,都是围绕这个目标服务的。使用时记住转写稿反复强调的边界:断言和类型检查不能替代运行时校验;泛型能复用类型,但要用约束限制“不能随便传”;遇到复杂需求先主线后细节,先让类型结构清楚,再逐步补实现。
课件图示








代码示例
示例一:类型断言(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会运行时报错。
常见错误
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。
- 现象:
联合类型里断言成
number再调toFixed,编译通过但运行崩- 现象:
(fo as number).toFixed(2)类型校验全绿,传入'100'时控制台报fo.toFixed is not a function。 - 原因:断言只是让编译器闭嘴,不改变运行时的实际值;调用方按联合类型合法地传了字符串。
- 修法:改用类型收窄
if (typeof fo === 'number') { ... },或函数重载 / 运行时校验后throw;断言只在“我确实比编译器知道得多”时使用。
- 现象:
泛型函数体里访问
obj.length报错- 现象:
Property 'length' does not exist on type 'T'。 - 原因:
T无约束时可以是任何类型,编译器无法保证有length。 - 修法:定义
interface HasLength { length: number },写成<T extends HasLength>;调用方传number时应当被拦下——如果没被拦下,说明约束写漏了。
- 现象:
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,避免两处各写一份。
- 现象:
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>')把数组转成字符串。
- 现象:
泛型接口“抽象了一半”:可变部分仍写死
- 现象:
ApiResponse定义了两个版本,字段code/message完全重复;后来改message类型只改了一处,另一个接口静默不一致。 - 原因:只把外层结构抽出来,没有把真正变化的那一段(
data)参数化。 - 修法:
interface ApiRes<T> { code: number; message: string; data: T },使用时ApiRes<User>、ApiRes<Goods>;判断标准是“除了某一段之外全都一样”就该上泛型。
- 现象:
练习
封装
function getEl<T extends HTMLElement>(selector: string): T,内部用querySelector+ 断言实现。然后回答:为什么调用方写成getEl<HTMLAnchorElement>('#link')之后就能直接.href?如果把泛型参数省略掉会发生什么?实现
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在这里替代了哪种常见错误。把综合案例里的
getList/setList重构成function createStorage<T>(key: string),返回{ get(): T[]; set(list: T[]): void }。要求:首次调用get()返回[]而不是报错;set()之后get()能取回同样结构的数据。改写视频 1 中的
toFixed案例,用typeof收窄让函数在传入字符串时编译期就报错(或运行时给出明确提示),而不是留到运行时报is not a function。对比说明“断言”和“收窄”在失败时机上的差别。用泛型约束实现
function getById<T extends { id: number }>(list: T[], id: number): T | undefined,然后尝试用getById(goodsList, 1)和一个没有id字段的数组分别调用,记录编译器分别给出什么反馈。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | TS核心-类型断言 | 07:22 |
| 2 | TS核心-泛型概念理解和泛型接口 | 10:47 |
| 3 | TS核心-泛型type和泛型函数 | 09:37 |
| 4 | TS核心-泛型约束 | 05:46 |
| 5 | TS综合小案例-01 | 08:42 |
| 6 | TS综合小案例-02 | 11:55 |
小结
- 类型断言的适用边界很窄:只有当你掌握编译器不掌握的信息时才用,且断言点越靠近数据来源越好。
- 断言能改变编译结果,改变不了运行时;凡是有外部输入(用户传参、localStorage、DOM 查询)参与的地方,优先收窄与校验,其次才是断言。
- 泛型的判断信号是“结构大部分相同、只有一小块不同”——把那一小块抽成参数,而不是复制一份类型。
- 泛型接口、泛型 type、泛型函数三者写法一致,差异只在关键字;
interface与type在对象注解场景下基本可互换。 extends在泛型里有两个身份:约束(T extends HasLength)和收窄键范围(K extends keyof T),都是把“随便传”变成“有范围地传”。- 类型定义应当“一处定义、多处引用”,
getList的返回类型和setList的入参类型必须同源,否则序列化/反序列化处最容易埋雷。 - 复杂需求按“先主线、后细节”推进:先把主流程和函数骨架摆出来,再逐个填坑并单独验证,能显著减少联调时的排查面。
- 泛型、断言这些手段解决的是类型层面的问题;运行时正确性仍然依赖你自己的分支判断(
null、空数组、首次访问)。