类、接口、继承、泛型 — TypeScript 的面向对象工具箱
类、接口、继承、泛型 — TypeScript 的面向对象工具箱
这一章讲什么: 上一章讲「类型是什么」,这一章讲「用类型组织代码」: 类与访问修饰符、接口合同、三种继承(类、抽象类、接口)、多态、泛型, 最后是 TypeScript 团队预制好的工具类型。 读完你能读懂 React 组件里最常见的 TypeScript 写法(泛型尤其多),也知道
tsconfig.json里那几个关键选项各管什么。
1. 顶层全景:一条「给轮子计数」的升级路线
原书这一组节只用了一条主线例子:一个 Vehicle(载具)基类,里面藏着
wheelCount(轮子数量)字段,各节不断改造它。我们照着这条路线走:
第 ① 站 类 + private 轮子数藏着,子类也拿不到 → 爆胎了改不了
第 ② 站 protected 子类能改、外部不能改 → 问题解了,但每个子类各写各的更新逻辑
第 ③ 站 抽象类 「必须实现 updateWheelCount」写进合同,子类不交作业编译不过
第 ④ 站 接口 + 多继承 连实现都不要,只要合同;一个接口能同时 extends 好几个
第 ⑤ 站 多态 + 泛型 对着合同写代码;类型本身也变成参数
图说:五站是同一件事的五个刻度——复用代码的渴望,一步步让步给「解耦」的渴望。2. 类与访问修饰符:private 只是编译期的君子协定
类是一组字段和方法的容器,可以 new 出实例。constructor(构造器)在
new 的瞬间运行,负责初始化字段1。实例方法里的 this 指向当前实例;
书里提醒:JavaScript 中 this 的值取决于函数怎么被调用,不是在哪定义,
所以方法脱离类单独传出去时 this 会变——本章只在类内部安全地使用它2。
默认情况下,类的所有成员都是 public(公开)——不显式标,就等于谁都能摸3。
改造主线例子:
class Vehicle {
constructor(private wheelCount: number) {}
showNumberOfWheels() { console.log(`wheels: ${this.wheelCount}`); }
}
class Motorcycle extends Vehicle { constructor() { super(2); } }
new Motorcycle().showNumberOfWheels(); // wheels: 2constructor(private wheelCount: number) 是一种省写,叫参数属性:
一行同时完成「声明一个私有字段」和「构造时赋值」两件事4。
接下来是本章最反直觉、也最该记住的一个区别:
TypeScript 的
private只在编译期生效。 它会在翻译出的 JavaScript 里消失, 运行时用括号访问等技巧仍然摸得到字段;而 JavaScript 自带的#私有字段 运行时真正私有5。
所以这本书的口径是:平时用 JavaScript 标准的 #,只在需要 TypeScript 特有
能力时才用 private5。配套的还有 readonly(只读):标了之后构造器之外
一律不许改,连类自己的方法改它都会报错6。
tsconfig.json 小插曲:为了用新语法,书里顺手讲了编译配置的四个常用选项——
target(翻译成哪个 ECMAScript 版本,6.0 起默认 ES2025)、lib(开发时可用的
API 提示,含 DOM)、module(模块格式,6.0 起默认从 CommonJS 改为 ESNext,
呼应生态全面转向 ESM)、strict(一个顶一组严格检查的总选项,6.0 起默认开)7。
这是 2026 年的时点口径,旧项目里的默认可能完全不同。
3. 接口与实现分离;接口是编译期的幽灵
接口是合同:只定成员的签名,不写实现;它只在编译期存在,不产出任何
JavaScript——你永远不会 new 一个接口8。
主线例子换到员工:Employee 接口有一个 getUniqueId 方法签名,
linda 和 pam 两个对象都符合这个接口,但各自的实现不同(一个返回
emp-2-linda,一个返回 mgr-1-pam)——同一结构,不同实现,这就是接口
的主要用途9。
接口还有个非常实用的场景:对接文档稀烂的第三方 API。不要图省事用 any
(第 02 章说过它会传染),而是只把你关心的字段写成接口;
得益于结构类型,运行时多出来的字段会被忽略,只要你在意的字段来了,
开发期的类型安全就保住了10。注意原书的提醒:null/undefined 字段仍要
自己处理好,运行时会因此抛异常10。
接口和 type 别名的分工(书里明写的三条区别11):同名接口会自动合并;
接口惯例上用来描述「类必须实现的形状」;接口描述不了并集等非对象类型,
type 可以。
4. 三种继承:类、抽象类、接口
类继承与 super
子类用 extends 继承父类;子类构造器里必须先调 super()(父类构造器)
才能碰 this12:
class Motorcycle extends Vehicle {
constructor() { super(2); } // 把 2 传给父类的 wheelCount
}主线例子在 ② 站遇到了真问题:子类想提供爆胎后的更新方法,却访问不了父类的
private wheelCount——子类继承了成员,也碰不到父类的私有成员,这是封装
的刻意设计13。解法是把 private 换成 protected:类自己和所有
子类可见,外部仍不可见14。
书里顺带提了一笔 namespaces(命名空间):老式的代码分组容器,把相关的类、
函数收在一个名字下面;现代代码用 ES 模块(import/export),命名空间只剩
在 legacy 代码里认得出它即可15。
抽象类:合同和实现装进同一个壳
接口没有实现,普通类全是实现;抽象类介于两者之间——可以有一部分成员 带真实现和状态,另一部分成员只有签名,留给子类填16:
abstract class Vehicle {
constructor(protected wheelCount: number) {}
abstract updateWheelCount(newWheelCount: number): void; // 只有签名
showNumberOfWheels() { console.log(`wheels: ${this.wheelCount}`); } // 有实现
}标了 abstract 的类不能被实例化;子类继承了却不实现抽象成员,编译器
拒绝通过。子类还可以覆盖(override)父类的非抽象方法,换上自己的版本17。
接口继承:一个接口能同时认多个爹
接口的继承比类自由:接口可以同时 extends 多个接口(interface C extends A, B),
类也可以同时 implements 多个接口(class X implements A, B, C)18。
类实现接口时没有 super()——接口是纯编译期的东西,翻译后什么都没有,
自然没有父构造器可调;implements 只做编译期检查,运行时节口早已消失19。
接口继承的两个顺带收获
书里的 interfaceInheritance.ts 例子还有两处值得点名。其一,Motorcycle 实现
Vehicle 接口时,必须连 Thing 的成员一起实现(getFullName)——接口的
extends 是「合同合并」,子接口的签名是父接口签名加上自己的,签字画押时一并生效20。
其二,运行结果只有一行 MC-beginner-cycle(getFullName 的返回值)——它提醒读者:
接口是编译期的存在,运行时只剩实现类的行为;你得到的输出里没有任何「接口」的踪影。
5. 多态与泛型:把「类型」也变成参数
多态(polymorphism,希腊语:多 + 形)的字面意思就值回票价: 对一般类型写代码,具体用哪个实现,运行时才知道;调用方不需要、 也不该关心21。
interface Animal { name: string; runMaxMiles(hours: number): number }
class Wolf implements Animal { runMaxMiles(hours: number) { return hours * 45; } }
class Cheetah implements Animal { runMaxMiles(hours: number) { return hours * 75; } }
function pickTheBestAnimalToRun(hours: number): number {
const animal: Animal = hours >= 0.5 ? new Wolf() : new Cheetah();
return animal.runMaxMiles(hours); // ← 多态就发生在这行
}animal.runMaxMiles(hours) 这一行,animal 背后是狼还是猎豹它都不关心——
以后再加一个 Horse implements Animal,这行代码一个字都不用改:
旧的代码对修改关闭、对扩展开放22。书里特意警告:例子里那些 instanceof
分支只为演示日志;真实代码里大量用 instanceof 判断具体类型,
恰恰是多态的反面——该让接口干活,而不是让调用方猜类型23。
又因为结构类型,连 implements Animal 不写都行:形状对了,自然兼容;
implements 的作用只是让编译器在声明处替你验证一遍形状24。
泛型是另一种多态(学名参数化多态):类型本身变成参数, 由使用方而不是定义方决定25。函数泛型长这样:
function getLength<T extends HasLength>(arg: T): number { return arg.length; }这条定义背后是一个重要教训:没加约束的 <T>,编译器对它一无所知——
像 unknown 一样,什么属性都不许碰。所以要用 extends 给它上约束:
「不管 T 是什么,必须有 length 属性」26。于是
getLength("Hello world.") 通过(字符串有 length),getLength<number>(22)
被拒绝(数字没有)。泛型也用在类和接口上:interface Vehicle<T>、
implements Vehicle<Wheels>——书明说:泛型在 React 开发里无处不在,
这是必须现在学它的原因27。
6. 工具类型:官方预制的类型变换
工具类型(utility types)是 TypeScript 团队预制的常用类型变换,标准库自带、 久经测试28。原书讲了三个,都是「从一个类型算出另一个类型」:
ReturnType<Type>:取出函数的返回类型,做成一个类型。type Result = ReturnType<typeof getData>把「getData返回什么」 变成可复用的类型别名——尤其适用于返回类型不受你控制的函数29。Pick<Type, Keys>:从一个大类型里挑出几个字段组成新类型。 面对 API 返回的 12 字段巨型对象,Pick<SuperComplexType, "name"|"age">只留你要的三个。注意它只作用于编译期,运行时数据一个字段不少30。Omit<Type, Keys>:反过来,剔除几个字段,留下其余31。
原书的建议值得原样带走:自己动手造轮子之前,先查文档看官方有没有现成的32。
7. 作者的判断与证据
- 「访问级别从最严的开始,需要时再放宽」「优先
#而非private」 「优先接口而非 any」——这一组是作者贯穿全章的工程口味,证据是编译器行为 本身(报错与否)加上实现层面的说明(私有字段编译后消失)5。 - 「
instanceof满天飞是多态的反面」是明确的判断句,不是工具说明23。 - 「工具类型先用官方的」是判断,附带理由:标准库、测试充分、广泛 使用32。
8. 边界与局限
- override 没标
override关键字:原书演示了覆盖但没提 TypeScript 的override显式标注(以及noImplicitOverride选项)——防「以为在覆盖、 其实父类早改名」的手段,属于本书没讲的常用进阶项。 - 命名空间讲了两句但没上模块:所有例子都用
namespace防重名, 原书自己承认这只是自包含演示的手段,真实代码该用 ES 模块15。 - 这一章的 OOP 讲法非常「经典 Java 味」;在 React 函数组件的世界里, 类与继承的实际用量远小于这里的热身,真正天天见的是接口和泛型(第 05、06 章)。
判断(我们的,不是书里的): 用同一条
Vehicle/wheelCount主线把 private → protected → abstract → interface 串起来,是这组原书节最好的设计—— 每一站都是被上一站的真实的痛逼出来的(爆胎改不了轮子数、子类不交作业、 连实现都不想要)。这比按特性字母表罗列的讲法有效得多。 如果错,会错在: 「经典 OOP 升级路线」的教学收益建立在读者接受 「继承是复用的默认答案」上;在组合优于继承成为主流口径的当下, 这条路线可能让读者高估继承在自己未来代码里的戏份。
9. 可带走的
- 类的成员默认
public;constructor(private x)是声明+赋值的省写。 - TypeScript 的
private编译后消失;要真私有用 JavaScript 的#。 protected= 类自己 + 子类可见;子类永远碰不到父类的private。- 接口只存在于编译期:不能
new、没有super、implements只是编译期检查。 - 接口可多重 extends、类可多重 implements——类继承没这个自由。
- 抽象类 = 部分实现 + 部分签名的壳;含抽象成员的类不可实例化。
- 多态的对偶警告:调用方写
instanceof猜类型,就是没用好接口。 - 泛型是「类型的参数」;无约束的
<T>按unknown对待,要用extends上约束。 ReturnType取函数返回类型,Pick挑字段,Omit删字段——先查官方工具再自造。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 参数属性与构造器 | Classes | text/15-fm-classes.txt:167(搜「As you can see, we updated the construct」) · text/15-fm-classes.txt:70(搜「initialize」) |
| private 编译期消失 vs # 运行时私有 | Classes | text/15-fm-classes.txt:300(搜「disappears in the emitted JavaScript」) |
| readonly | Classes | text/15-fm-classes.txt:328(搜「not allowed once you use」) |
| tsconfig 四选项 | Classes | text/15-fm-classes.txt:452(搜「target」) · text/15-fm-classes.txt:568(搜「strict」) |
| 接口不产出 JavaScript | Interfaces | text/16-fm-interfaces.txt:74(搜「does not emit」) |
| 接口 vs type 三条区别 | Interfaces | text/16-fm-interfaces.txt:31(搜「automatically merge」) |
| 第三方 API 只写关心字段 | Interfaces | text/16-fm-interfaces.txt:205(搜「only the fields」) |
| 子类碰不到父类 private | Interface inheritance | text/16-fm-interfaces.txt:444(搜「It's because we are trying to access a p」) |
| protected | Interface inheritance | text/16-fm-interfaces.txt:504(搜「type allows the class and any inheriting」) |
| 命名空间是 legacy | Interface inheritance | text/16-fm-interfaces.txt:550(搜「a legacy feature」) |
| 抽象类定义 | Abstract classes | text/17-fm-abstract-classes.txt:3(搜「no implementation, no state」) · text/17-fm-abstract-classes.txt:183(搜「both like a regular class」) |
| 实现接口没有 super | Interface inheritance | text/18-fm-interface-inheritance.txt:147(搜「Unlike extending a class, there is no」) |
| 多态与开闭 | Interface inheritance | text/18-fm-interface-inheritance.txt:338(搜「regardless」) · text/18-fm-interface-inheritance.txt:366(搜「closed」) |
| instanceof 是多态的反面 | Interface inheritance | text/18-fm-interface-inheritance.txt:384(搜「opposite of polymorphism」) |
| 泛型 = 类型的参数 | Interface inheritance | text/18-fm-interface-inheritance.txt:444(搜「allow a type definition to include an as」) |
| 无约束 T 按 unknown 对待 | Interface inheritance | text/18-fm-interface-inheritance.txt:545(搜「unconstrained」) |
| 泛型无处不在(React) | Interface inheritance | text/18-fm-interface-inheritance.txt:766(搜「everywhere」) |
| ReturnType | ReturnType<Type> | text/19-fm-returntype-type.txt:13(搜「create a type from the type」) |
| Pick(含编译期说明) | Pick<Type, Keys> | text/20-fm-pick-type-keys.txt:48(搜「type SimpleType = Pick」) · text/20-fm-pick-type-keys.txt:89(搜「does not filter out」) |
| Omit 与「先查官方」 | Omit<Type, Keys> | text/21-fm-omit-type-keys.txt:28(搜「type SimpleTypeOmit」) · text/21-fm-omit-type-keys.txt:61(搜「check the documentation」) |
Footnotes
-
出处:「Classes」第 62–70 段(
text/15-fm-classes.txt:62,搜「constructor」;:70,搜「initialize」)。 ↩ -
出处:「Classes」第 86–96 段(
text/15-fm-classes.txt:94,搜「depends on how a function is called」)。 ↩ -
出处:「Classes」第 142–150 段(
text/15-fm-classes.txt:142,搜「default accessibility」)。 ↩ -
出处:「Classes」第 167–175 段(
text/15-fm-classes.txt:173,搜「parameter」;:175,搜「two things in one line」)。 ↩ -
出处:「Classes」第 294–306 段(
text/15-fm-classes.txt:298,搜「only enforced at compile time」;:304,搜「truly private at runtime」)。 ↩ ↩2 ↩3 -
出处:「Classes」第 320–332 段(
text/15-fm-classes.txt:328,搜「not allowed once you use」)。 ↩ -
出处:「Classes」第 454–470 段与 568–638 段(
text/15-fm-classes.txt:452,搜「target」;:636,搜「strict defaults to true」)。 ↩ -
出处:「Interfaces」第 74–78 段(
text/16-fm-interfaces.txt:74,搜「does not emit any JavaScript」)。 ↩ -
出处:「Interfaces」第 94–119 段与 169 段(
text/16-fm-interfaces.txt:169,搜「different implementations」)。 ↩ -
出处:「Interfaces」第 187–221 段(
text/16-fm-interfaces.txt:205,搜「only the fields」;:221,搜「exceptions during runtime」)。 ↩ ↩2 -
出处:「Interfaces」第 27–39 段(
text/16-fm-interfaces.txt:31,搜「automatically merge」)。 ↩ -
出处:「Interface inheritance」第 328–354 段(
text/16-fm-interfaces.txt:334,搜「In a subclass constructor, you must call」)。 ↩ -
出处:「Interface inheritance」第 440–452 段(
text/16-fm-interfaces.txt:444,搜「It's because we are trying to access a p」)。 ↩ -
出处:「Interface inheritance」第 482–504 段(
text/16-fm-interfaces.txt:504,搜「type allows the class and any inheriting」)。 ↩ -
出处:「Interface inheritance」第 506–560 段(
text/16-fm-interfaces.txt:550,搜「a legacy feature」)。 ↩ ↩2 -
出处:「Abstract classes」第 3–11 段(
text/17-fm-abstract-classes.txt:3,搜「no implementation, no state」)。 ↩ -
出处:「Abstract classes」第 183–203 段(
text/17-fm-abstract-classes.txt:185,搜「cannot instantiate」;:203,搜「instead」)。 ↩ -
出处:「Interface inheritance」第 15–27 段(
text/18-fm-interface-inheritance.txt:15,搜「multiple other interfaces」)。 ↩ -
出处:「Interface inheritance」第 141–157 段(
text/18-fm-interface-inheritance.txt:147,搜「Unlike extending a class, there is no」;:155,搜「compile-time check」)。 ↩ -
出处:「Interface inheritance」第 159–185 段(
text/18-fm-interface-inheritance.txt:167,搜「must implement both」;:179,搜「because Vehicle」)。 ↩ -
出处:「Interface inheritance」第 203–215 段(
text/18-fm-interface-inheritance.txt:207,搜「poly = many」;:213,搜「doesn't need to know」)。 ↩ -
出处:「Interface inheritance」第 336–372 段(
text/18-fm-interface-inheritance.txt:366,搜「closed」)。 ↩ -
出处:「Interface inheritance」第 374–384 段(
text/18-fm-interface-inheritance.txt:384,搜「opposite of polymorphism」)。 ↩ ↩2 -
出处:「Interface inheritance」第 408–428 段(
text/18-fm-interface-inheritance.txt:408,搜「by shape, not by name」)。 ↩ -
出处:「Interface inheritance」第 430–450 段(
text/18-fm-interface-inheritance.txt:444,搜「chosen by the user」)。 ↩ -
出处:「Interface inheritance」第 539–563 段与 584–626 段(
text/18-fm-interface-inheritance.txt:545,搜「is unconstrained, TypeScript treats it m」;:592,搜「T extends HasLength」)。 ↩ -
出处:「Interface inheritance」第 764–766 段(
text/18-fm-interface-inheritance.txt:766,搜「everywhere」)。 ↩ -
出处:「Omit<Type, Keys>」第 57–59 段(
text/21-fm-omit-type-keys.txt:59,搜「well-tested」)。 ↩ -
出处:「ReturnType<Type>」第 3–13 段与 48 段(
text/19-fm-returntype-type.txt:5,搜「don't control directly」;:48,搜「type Result = ReturnType」)。 ↩ -
出处:「Pick<Type, Keys>」第 48–57 段与 85–91 段(
text/20-fm-pick-type-keys.txt:89,搜「does not filter out」)。 ↩ -
出处:「Omit<Type, Keys>」第 3–9 段与 28–55 段(
text/21-fm-omit-type-keys.txt:3,搜「removing」)。 ↩ -
出处:「Omit<Type, Keys>」第 61 段(
text/21-fm-omit-type-keys.txt:61,搜「check the documentation」)。 ↩ ↩2