主题
TypeScript 核心(二)
本页对应 TypeScript 核心,共 6 段视频,约 1 小时 4 分。
学习目标
- 能编写带可选参数和无返回值(void)的函数,并说明可选参数必须位于参数列表末尾、void 与 undefined 的区别。
- 能使用 interface 定义对象类型,并通过问号设置可选属性、通过 extends 实现接口继承。
- 能使用 type 定义对象类型,并用交叉类型(&)模拟继承,能对比 interface 与 type 的异同并做出选择。
- 能使用字面量类型配合联合类型限定变量的可选范围,并解释 const 与 let 声明字面量时的类型推断差异。
- 能说明 TypeScript 类型推论机制,在变量声明和函数返回值场景中判断推断结果,并解释 any 的风险及避免使用的原因。
- 能根据给定的嵌套响应对象或表单数据,选择 interface 或 type 完成类型定义并通过类型校验。
知识点
函数类型:可选参数与无返回值
为什么需要:函数参数一旦注解类型,默认就是必填项;但业务里常有“可传可不传”的参数。函数也可能只做打印、遍历等动作,没有返回值,需要明确标注。
怎么想:可选参数表示“可传可不传;一旦传了,类型必须正确”。写法是在参数名后、冒号前加 ?。无返回值函数用 void 标注,表示该函数不返回结果。
怎么做:
ts
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return `${firstName}${lastName}`;
}
return firstName;
}
buildName("A"); // 合法
buildName("A", "B"); // 合法
// buildName("A", 123); // 报错:number 不能赋给 string
function printArr(arr: number[]): void {
arr.forEach(item => console.log(item));
}调试时可以看到:只传 "A" 输出 A;传 "A","B" 输出 AB;第二个参数传数字会报类型错误。void 函数若用变量接收,得到的是 void 类型。
易错点:可选参数必须放在所有必选参数末尾;把可选参数提前会报“必选参数不能位于可选参数之后”。另外,void 与 undefined 不是一回事:void 表示没有返回值;若函数返回值标为 undefined,则必须 return undefined。
interface:描述对象形状与业务场景
为什么需要:对象数据需要约束属性名、属性数量和属性类型。前后端通信时,表单提交、列表渲染都依赖稳定的数据结构。
怎么想:把 interface 理解为“对象形状说明书”:属性不能多、不能少,类型要匹配。典型场景有两个:前端向后端提交表单时按接口文档定义字段;前端渲染后端列表时获得智能提示。
怎么做:
ts
interface Person {
name: string;
age: number;
}
const p: Person = { name: "Jack", age: 18 };
interface Form {
username: string;
password: string;
}
const form: Form = { username: "jack", password: "123456" };调试时,少写 name、多写 hobby、把 age 写成字符串都会报错。渲染列表时,输入 item. 能自动提示属性,减少查文档和拼写错误。
易错点:interface 约束的是对象结构,不是随便一个值;属性多一个少一个都不行,类型也必须一致。
interface 的可选属性与继承
为什么需要:对象里有些属性可传可不传,例如按钮的 size;多个接口有重复字段时,需要复用类型。
怎么想:可选属性在属性名后加 ?;继承用 extends,子接口拥有父接口属性,并可追加自己的属性。
怎么做:
ts
interface Props {
type: string;
size?: string;
}
const props1: Props = { type: "success" };
const props2: Props = { type: "success", size: "large" };
interface GoodsType {
id: string;
price: number;
}
interface DiscountGoodsType extends GoodsType {
discountPrice: number;
}调试时,size 不传合法,传了但写成数字会报错;继承后的对象缺少 id 或 discountPrice 都会报错。
易错点:可选属性不是“随便类型”,传了仍要满足类型;继承来的属性和自身属性都不能缺失。
type:对象类型、交叉类型与选择
为什么需要:对象类型除了 interface,还可以用类型别名 type 描述;type 还能配合交叉类型模拟继承。
怎么想:type 后跟名称和等号,再写对象结构;继承模拟用 & 连接父类型和新增结构。同名 interface 会合并,同名 type 会报错;讲师建议对象注解优先用 type,因为它更灵活。
怎么做:
ts
type Person = { name: string; age: number };
const p2: Person = { name: "Jack", age: 18 };
type GoodsType = { id: string; price: number };
type DiscountGoodsType = GoodsType & { discountPrice: number };调试时,type 定义的对象同样不能多属性、少属性或类型不符;交叉类型后的对象必须同时具备 id、price、discountPrice。
易错点:type 同名会直接报错;交叉类型不是“或”,而是“同时满足”。
字面量类型与联合类型
为什么需要:普通 string、number 太宽泛。性别只有“男/女”,状态码只有几个值,需要精确可选范围。
怎么想:字面量类型用具体值作类型,通常和联合类型配合,限定只能取某几个值。
怎么做:
ts
type Gender = "男" | "女";
let gender: Gender = "男";
// gender = "哈哈"; // 报错
type Code = 1001 | 1002 | 1003;
let code: Code = 1001;
// code = 1004; // 报错调试时,输入变量会有“男/女”提示;写其他字符串或数字会报错。let str = "123" 推断为 string,const str = "456" 推断为字面量 "456"。
易错点:字面量类型不单独乱用,通常配联合类型;const 与 let 的推断结果不同。
类型推论与 any
为什么需要:每次手写类型注解很繁琐;TS 会根据赋值和返回值自动推断类型,减少代码量。any 则用于彻底关闭类型检查,但会失去类型提示。
怎么想:声明变量并赋值、函数返回值都可被推断。初学建议先写类型,熟练后能省略再省略;不确定变量类型时,把鼠标移上去看提示。any 尽量不用。
怎么做:
ts
let count = 100; // 推断为 number
// count = false; // 报错
function add(a: number, b: number) {
return a + b; // 返回值推断为 number
}
let value: any = 100;
value = "x";
value = false;调试时,count 再赋布尔值会报错;add 的返回值被推断为 number。any 变量随便赋值、随便赋给别人都不报错,但输入 value. 也没有可靠提示。
易错点:any 不是“万能好类型”,它让 TS 退回到类似 JS 的无类型检查状态,漏洞风险更高,应尽量避免。
先抓住定义:TS 是“具有类型语法的 JavaScript”
为什么需要: JavaScript 写起来灵活,但变量、函数参数到底该传什么,往往要运行起来才发现问题。TypeScript 在 JavaScript 语法上增加类型语法,让我们在开发阶段就能表达“这里应该是什么类型”,并借助类型检查提前发现不一致。
怎么想: 先把 TS 理解成“JavaScript + 类型语法”,不要一上来把它当成完全陌生的语言。笔记里的原话是:“TypeScript 是具有类型语法的 JavaScript,是一门强类型的编程语言。”这句话有两个重点:一是仍然写 JavaScript 风格的代码;二是类型会成为约束。

怎么做: 从最简单的变量注解开始。转写稿中的 count 例子可以还原为:
ts
let count: number = 100;
count = 200; // 可以
// count = false; // 报错:不能将布尔类型分配给 number这里 : number 就是类型注解。它表达的不是“运行时把 count 变成 number”,而是开发时告诉 TS:这个变量只能接收 number 类型的数据。
易错点: 不要把类型注解当成运行时逻辑。类型写错时,编辑器会出现红色波浪线,鼠标移上去能看到“不能将某某类型分配给某某类型”的提示;这不是等到页面运行才暴露的问题。
强类型体现在哪里:参数、返回值和调用检查
为什么需要: 函数是业务代码里最常复用的单位。参数类型不明确时,调用方可能传错;返回值不明确时,接收方也可能用错。转写稿里讲函数类型时反复强调:参数一旦注解类型,就既要满足类型要求,也默认是必填项。
怎么想: 先确定函数的“输入”和“输出”。参数是输入,返回值是输出。输入输出确定后,函数体内部和外部调用都有了约束。
怎么做: 用转写稿里的求和函数说明:
ts
function add(a: number, b: number) {
return a + b;
}
const result = add(1, 2); // result 被推断为 number参数 a、b 都注解为 number,所以调用时传数字才符合要求。转写稿还提到可选参数:如果某个参数可传可不传,可在冒号前加问号,例如 lastName?: string;一旦传了,就必须满足 string 类型。
易错点: 可选参数不是随便放。转写稿中特意演示了把可选参数提到前面会报错,提示“必选参数不能位于可选参数之后”。所以可选参数要放在所有参数的末尾。另一个易错点是:可选参数“可不传”,但传了就不能传错类型。
类型推断:不是每处都要手写类型
为什么需要: 如果每个变量、每个返回值都手写类型,代码会很啰嗦。TS 提供了类型推断机制:在没有显式写类型注解时,它也会根据初始值或上下文给出类型。
怎么想: 有初始值时,先让 TS 自己推断;推断结果不清楚,或者团队规范要求显式表达时,再补注解。转写稿里说得很直白:类型推断体现了“不用手写,但也会自动添加类型”的机制,目的是让开发者少写代码。
怎么做: 变量赋值场景:
ts
let count = 100; // 推断为 number
count = 200; // 可以
// count = false; // 报错函数返回值场景:
ts
function add(a: number, b: number) {
return a + b;
}因为 a、b 都是 number,a + b 也是 number,所以返回值不用再手写,TS 会推断为 number。
易错点: 初学阶段建议先多写类型注解,熟悉后再省略“能省略的地方”。转写稿还给了一个实用调试技巧:把鼠标移到变量上,看它当前被推断成什么类型;不知道类型时,这个办法很快。
any:看似省事,实际会失去类型检查
为什么需要: 转写稿提到,有些开发者为了省事会用 any。它可以让变量跳过类型检查,赋值、访问属性、调用函数都不容易报类型错误,因此常被拿来应付临时场景。
怎么想: any 的本质是“放弃类型约束”。TS 最核心的价值是类型检查与类型提示;一旦大量使用 any,就接近回到普通 JavaScript 的体验,程序里潜在的问题也更难提前发现。
怎么做: 转写稿的演示可以概括为:
ts
let obj: any = 100;
obj = "字符串"; // 可以
obj = false; // 可以
let num: number = 100;
num = obj; // 可以,因为 obj 是 any易错点: any 不是“万能安全类型”,而是关闭检查。转写稿明确建议实际开发中尽量避免使用它。另一个易错点是:用了 any 后,点语法提示也会变弱,享受不到 TS 的类型提示。
本节调试方法:用报错和悬停验证理解
为什么需要: 只看定义容易停留在概念上。转写稿里多次通过“故意写错—看红色波浪线—读提示”的方式验证类型规则,这比死记结论更可靠。
怎么想: 先写一个符合类型的值,再写一个不符合类型的值,观察 TS 在哪里报错、报错信息说“不能将什么分配给什么”。同时把鼠标移到变量或函数返回值上,看推断结果。
怎么做: 例如先写 let count: number = 100,再把 count 赋成 false,看编辑器提示;再写 function add(a: number, b: number) { return a + b },把鼠标移到调用结果上,确认它是 number。
易错点: 读报错时要抓住“类型不匹配”这个核心,不要只看到红色波浪线就慌。转写稿里的调试过程也说明:TS 的报错是在帮我们定位类型问题,而不是代码一定完全不能运行。
先看本节要解决的问题
这一节笔记只有两个要点:静态类型检查能提前发现代码错误;良好的代码提示能提升开发效率。它们看起来像“体验优化”,其实是 TypeScript 最直接的价值:把很多原本要等运行时才暴露的问题,提前到写代码和保存文件时暴露;把原本要翻接口文档、靠记忆拼字段的工作,变成编辑器里的自动联想。
为什么需要静态类型检查
JavaScript 里,参数传错、对象字段拼错、类型不一致,往往要等函数执行到那一行才报错。TypeScript 的思路是:只要你能把“这里应该是什么类型”写清楚,编辑器就能在保存时先替你检查一遍。
转写稿里有一个很典型的调试过程:build 函数负责合并两个名字,第二个参数 lastName 写成可选参数后,不传可以;传一个字符串 B 也可以;但如果传数字,编辑器马上提示“number 不能赋值给 string 类型的参数”。这就是“要么别传,传了就必须类型正确”。

对象场景更明显。用 interface 约束一个对象后,少一个 name 属性会报“缺少属性”;多一个 hat 属性也会报错;age 本来要求 number,写成带引号的字符串也会报错。也就是说,属性的数量、名称和类型都被提前约定了。

怎么想:把“约定”提前写成类型
不要等到运行失败才想“这里应该传什么”。写函数时先想参数和返回值;写表单时先想后端接口要什么字段;写列表时先想每一项对象长什么样。类型不是装饰,而是把口头约定变成可检查的规则。
比如字面量联合类型可以限制取值范围:性别只能是“男”或“女”,传“哈哈哈”就会报错;接口返回的 code 只允许 1001、1002、1003 时,传 1004 也会被拦住。这样错误在开发阶段就暴露,而不是等用户点到某个页面才发现。
怎么做:让报错在保存时就出现
第一,给关键参数、返回值、对象结构加注解;第二,保存后看编辑器里的红色波浪线;第三,把鼠标移到报错处看具体原因;第四,修正调用处或类型定义。转写稿里的操作就是“改代码—保存—看右侧结果/看波浪线—悬停读错误—再修正”。
易错点:别把类型检查关掉
最常见的逃避方式是随手写 any。转写稿明确演示了:变量一旦是 any,可以随便赋值,也可以赋给其他类型,但同时也享受不到类型提示,相当于又回到了没有类型检查的状态。另一个易错点是可选参数:不传可以,但一旦传了,类型仍必须正确;可选参数还应放在参数末尾。
为什么需要良好的代码提示
代码提示的价值在“少查、少记、少拼错”。转写稿里模拟商品列表时,给列表项加上类型后,输入 item. 会自动提示 id、price;渲染后端返回的嵌套对象时,先点外层属性,再点内层对象,title、content 也能被提示出来。以前可能要截图接口文档、对照着写,现在编辑器直接告诉你有哪些字段。

怎么想:类型是提示的来源
提示不是凭空来的。你先用 interface 或 type 描述对象结构,编辑器才知道这个对象身上有哪些属性;你给数组加上元素类型,遍历时才知道 item 是什么。类型定义越贴近后端接口,提示就越准。
怎么做:先注解,再使用点语法
先定义表单、列表项、接口返回值的类型,再把它注解到变量、数组或函数返回值上。之后在遍历、渲染、取值时输入点语法,观察自动联想;选中需要的字段,减少手写拼错。转写稿中 forEach 遍历时输入 item. 能提示 id 和 price,就是这一过程。
易错点:提示不能代替运行时校验
提示只能说明“类型上可能有这个字段”,不能保证后端一定返回。可选属性在提示里可能出现,但使用时仍要判断是否存在。若类型定义写错,提示也会跟着错;若用了 any,提示会消失。因此类型定义要跟接口文档保持一致,关键数据仍要做运行时处理。
把两个好处合起来看
静态类型检查负责“提前拦住明显错误”,代码提示负责“让正确的写法更快出现”。前者提升代码质量,后者提升开发效率。转写稿里的参数报错、对象属性报错、字面量范围限制,以及商品列表和嵌套对象的自动提示,都是这两个好处的具体表现。学习这一节,不是背概念,而是养成一个习惯:先把类型约定写下来,再让编辑器帮你检查和提示。
课件图示








代码示例
示例一:函数类型——可选参数与无返回值(void)
ts
// 可选参数:参数名后加 ?,表示可传可不传;传了必须类型正确
function build(firstName: string, lastName?: string): string {
if (lastName) {
return `${firstName} ${lastName}`
}
return firstName
}
console.log(build('A')) // A
console.log(build('A', 'B')) // A B
// console.log(build('A', 123)) // 报错:number 不能赋值给 string
// 无返回值:用 void 注解
function h(arr: number[]): void {
arr.forEach(item => console.log(item))
}
h([1, 2, 3]) // 依次打印 1 2 3
// 调用后无法用变量接收有意义的返回值
const r = h([1])
console.log(r) // undefined(但 r 的类型是 void,不是 undefined 类型)要点与易错点
lastName?: string中问号必须写在冒号前面,写成lastName: ?string是错的。- 可选参数必须放在所有必选参数的末尾,否则报错“必选参数不能位于可选参数之后”。
void与undefined不是一回事:void表示函数无返回值;如果把返回值标注成undefined,则函数内部必须return undefined。void函数不要尝试用变量去接收并参与运算,类型上拿不到有效值。
示例二:interface——可选属性与接口继承
ts
// 可选设置:size 加问号后,赋值时可省略
interface Props {
type: string
size?: string
}
const p1: Props = { type: 'success' } // 不传 size,合法
const p2: Props = { type: 'success', size: 'large' } // 传了,必须是 string
// const p3: Props = { type: 'success', size: 123 } // 报错
// 接口继承:子接口复用父接口属性
interface GoodsType {
id: string
price: number
}
interface DisGoodsType extends GoodsType {
disPrice: number
}
const goods: DisGoodsType = {
id: '1001',
price: 200,
disPrice: 180
}
// 少 id 报错:缺少属性 id
// 少 disPrice 报错:缺少属性 disPrice
// 多写一个属性同样报错:对象字面量只能指定已知属性要点与易错点
- 可选属性传值时,值的类型仍要匹配,
size?: string传123会报错。 extends后写的是父接口名,子接口里只写自己新增的属性,不要重复父接口已有的id、price。- 给对象字面量赋值时,属性不能多也不能少,这是 interface 校验最容易被忽略的一点。
示例三:type——交叉类型模拟继承、嵌套对象与字面量联合类型
ts
// type 配合交叉类型(&)模拟继承
type GoodsType = {
id: string
price: number
}
type DisGoodsType = GoodsType & {
disPrice: number
}
const goods: DisGoodsType = {
id: '1001',
price: 200,
disPrice: 180
}
// 嵌套对象:先定义内层,再定义外层
type Data = {
title: string
content: string
}
type Res = {
code: number
message: string
data: Data
}
const res: Res = {
code: 200,
message: 'success',
data: { title: '标题', content: '内容' }
}
// 字面量 + 联合类型:精确的可选范围
type Gender = '男' | '女'
let g: Gender = '男'
// g = '哈哈' // 报错:不能将 "哈哈" 分配给 Gender
type Code = 1001 | 1002 | 1003
type Res2 = {
code: Code
message: string
}
const r2: Res2 = { code: 1001, message: 'ok' }
// const r3: Res2 = { code: 1004, message: 'ok' } // 报错:1004 不在可选范围内要点与易错点
type定义对象类型时必须写等号:type X = { ... },不要写成type X { ... }。- 交叉类型用
&连接,顺序不影响结果;GoodsType & { disPrice: number }等价于继承后再扩展。 - 嵌套对象建议先里后外定义,外层直接引用内层类型名,不要在一个大括号里硬嵌套。
- 字面量类型通常不单独使用,要和联合类型
|配合,才能既精确又实用。 type同名会直接报错;interface同名会合并(属性取并集,类型冲突才报错),这是二者最明显的差异之一。
常见错误
1. 可选参数写在必选参数前面
- 现象:
function f(a?: string, b: string)直接标红,提示"必选参数不能位于可选参数之后"。 - 原因:TS 按位置匹配实参,可选参在前会导致后面的必选参无法确定是否被传值。
- 修法:把带
?的参数全部挪到参数列表末尾;实在需要"中间可省略",改成传对象配置或显式传undefined。
2. 把 void 和 undefined 当成一回事
- 现象:给无返回值的函数标
: undefined,或写return undefined后仍报类型不符;反过来把返回undefined的函数标成: void又想去取返回值。 - 原因:
void表示"不关心返回值",undefined是一个具体类型,函数标: undefined时必须return undefined。 - 修法:纯执行副作用的函数用
: void;确实要返回undefined才用: undefined。并且不要用变量去接收void函数的返回值。
3. 用 interface 描述的对象赋值时"多一个/少一个属性"就报错
- 现象:接口定义了
name、age,赋值时多写一个hobby或漏写age,都标红。 - 原因:interface 约束的是对象"形状",属性名、数量、类型三者都要精确匹配,不是"包含即可"。
- 修法:确实可传可不传的字段加
?;需要扩展字段用extends或&派生新类型,而不是硬塞进原接口。
4. 同名 interface 声明合并引发意外属性冲突
- 现象:两个文件里都写了
interface Item,合并后多出字段;若两处同名属性类型不同,直接报"类型必须为 xxx"。 - 原因:同名 interface 会自动声明合并(属性并集),而同名
type会直接报重复定义。 - 修法:全局接口注意命名唯一;不希望被合并时统一改用
type。
5. 用 type 模拟继承时漏写交叉连接符
- 现象:
type Sub = Base { extra: number }报语法错误,或只写了{ extra: number }导致父类型字段全部丢失。 - 原因:
type没有extends,复用必须靠交叉类型&组合。 - 修法:写成
type Sub = Base & { extra: number };注意&是"且",不是"或"。
6. 该用字面量联合的地方写成了宽泛基础类型
- 现象:
code: number或gender: string时,传1004、"哈哈哈"都能通过校验,运行时后端报错。 - 原因:
number/string只约束大类,不约束取值范围;精确范围要靠字面量类型 + 联合类型。 - 修法:
type Code = 1001 | 1002 | 1003,type Gender = '男' | '女',再注解到字段/参数上。
7. 依赖类型推论时用了空初始值
- 现象:
let list = []之后list.push(1)报错(推断成never[]),或let obj = {}后加点赋值报错。 - 原因:推论只看初始值,空值推不出元素/属性类型。
- 修法:给变量显式注解
let list: number[] = []、let obj: Goods = {} as Goods(或先给完整初始值)。
8. 图省事到处写 any
- 现象:注解
any后属性名拼错、调用不存在的方法、把any赋值给number全都不报错,问题被推迟到运行时报错。 - 原因:
any会关闭该值的全部类型检查并向下传染,也丢失代码提示。 - 修法:先试着写具体类型或
unknown(unknown使用前必须收窄);第三方库缺类型时优先找@types/*或declare补声明。
练习
练习 1(找错) 下面代码有两处类型问题,指出并改正:
ts
function buildName(lastName?: string, firstName: string): void {
return firstName + lastName
}提示:看参数顺序和返回值注解是否与函数体一致。
练习 2(interface 建模) 定义一个 Goods 接口(id: string、price: number),再定义 DiscountGoods 继承它并新增 discountPrice: number;最后声明一个 DiscountGoods 类型变量并正确赋值。要求:id 改成可选属性后再验证一次赋值是否仍合法。
练习 3(type 等价改写) 把练习 2 用 type + 交叉类型重写一遍,并说明在"对象类型注解"场景下你团队应统一用哪种写法、理由是什么。
练习 4(字面量联合) 后端返回体形如 { code: 1001 | 1002 | 1003, message: string, data: { title: string; content: string } }。请先定义内层 data 类型,再定义外层类型(分别用 type 实现),并说明如果 code 写成 number 会有什么后果。
练习 5(推论与 any) 说明下列代码为何报错,并给出两种修复方式:
ts
let ids = []
ids.push(1001)再补一句:如果把 ids 注解成 any[],这段代码会怎样?代价是什么?
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | TS核心-函数类型-可选参和无返回值 | 12:26 |
| 2 | TS核心-interface-概念和基本使用 | 11:09 |
| 3 | TS核心-interface-可选设置和继承 | 11:32 |
| 4 | TS核心-type注解对象类型 | 13:16 |
| 5 | TS核心-字面量类型 | 08:53 |
| 6 | TS核心-类型推论和any类型 | 07:29 |
小结
- 函数类型注解只做两件事:约束参数(含可选参
?,且必须置于末尾)和约束返回值(无返回值用void,不是undefined)。 interface约束的是对象的精确"形状":属性名、数量、类型三者同时匹配,多一个少一个都不行。- 可选属性
?与可选参数?语义一致:可以不给,给了就必须类型正确。 - 类型复用的两条路:
interface用extends,type用&交叉类型;对象注解场景推荐统一用type,因为它更灵活(还能表达联合、字面量、基础类型别名)。 - 同名
interface会声明合并(属性并集,冲突即报错),同名type直接报重复定义——这是选型时要考虑的差异点。 - 字面量类型极少单独使用,标准姿势是与联合类型组合,把"任意 string/number"收窄为"有限可选范围",如性别、状态码、按钮
type。 - 类型推论是"少写注解"的福利(变量初始化、函数返回值都能推),但空数组/空对象推不出有效类型,必须显式注解;拿不准时把鼠标悬停在变量上查看推断结果。
any是逃生舱不是常规手段:它关闭检查、丢失提示、还会传染,能写具体类型就不写any,需要"未知"时用unknown。所有类型约束只在编译期生效,运行时依然要靠代码自身的防御。