跳转至

类、接口、继承、泛型 — TypeScript 的面向对象工具箱

这一章讲什么: 上一章讲「类型是什么」,这一章讲「用类型组织代码」: 类与访问修饰符、接口合同、三种继承(类、抽象类、接口)、多态、泛型, 最后是 TypeScript 团队预制好的工具类型。 读完你能读懂 React 组件里最常见的 TypeScript 写法(泛型尤其多),也知道 tsconfig.json 里那几个关键选项各管什么。

1. 顶层全景:一条「给轮子计数」的升级路线

原书这一组节只用了一条主线例子:一个 Vehicle(载具)基类,里面藏着 wheelCount(轮子数量)字段,各节不断改造它。我们照着这条路线走:

  第 ① 站  类 + private      轮子数藏着,子类也拿不到 → 爆胎了改不了
  第 ② 站  protected          子类能改、外部不能改 → 问题解了,但每个子类各写各的更新逻辑
  第 ③ 站  抽象类              「必须实现 updateWheelCount」写进合同,子类不交作业编译不过
  第 ④ 站  接口 + 多继承       连实现都不要,只要合同;一个接口能同时 extends 好几个
  第 ⑤ 站  多态 + 泛型          对着合同写代码;类型本身也变成参数

  图说:五站是同一件事的五个刻度——复用代码的渴望,一步步让步给「解耦」的渴望。

2. 类与访问修饰符:private 只是编译期的君子协定

类是一组字段和方法的容器,可以 new 出实例。constructor(构造器)在 new 的瞬间运行,负责初始化字段2。实例方法里的 this 指向当前实例; 书里提醒:JavaScript 中 this 的值取决于函数怎么被调用,不是在哪定义, 所以方法脱离类单独传出去时 this 会变——本章只在类内部安全地使用它3。

默认情况下,类的所有成员都是 public(公开)——不显式标,就等于谁都能摸4。 改造主线例子:

class Vehicle {
    constructor(private wheelCount: number) {}
    showNumberOfWheels() { console.log(`wheels: ${this.wheelCount}`); }
}
class Motorcycle extends Vehicle { constructor() { super(2); } }
new Motorcycle().showNumberOfWheels();   // wheels: 2

constructor(private wheelCount: number) 是一种省写,叫参数属性: 一行同时完成「声明一个私有字段」和「构造时赋值」两件事5。

接下来是本章最反直觉、也最该记住的一个区别:

TypeScript 的 private 只在编译期生效。 它会在翻译出的 JavaScript 里消失, 运行时用括号访问等技巧仍然摸得到字段;而 JavaScript 自带的 # 私有字段 运行时真正私有6。

所以这本书的口径是:平时用 JavaScript 标准的 #,只在需要 TypeScript 特有 能力时才用 private6。配套的还有 readonly(只读):标了之后构造器之外 一律不许改,连类自己的方法改它都会报错7。

tsconfig.json 小插曲:为了用新语法,书里顺手讲了编译配置的四个常用选项—— target(翻译成哪个 ECMAScript 版本,6.0 起默认 ES2025)、lib(开发时可用的 API 提示,含 DOM)、module(模块格式,6.0 起默认从 CommonJS 改为 ESNext, 呼应生态全面转向 ESM)、strict(一个顶一组严格检查的总选项,6.0 起默认开)8。 这是 2026 年的时点口径,旧项目里的默认可能完全不同。

3. 接口与实现分离;接口是编译期的幽灵

接口是合同:只定成员的签名,不写实现;它只在编译期存在,不产出任何 JavaScript——你永远不会 new 一个接口9。

主线例子换到员工:Employee 接口有一个 getUniqueId 方法签名, linda 和 pam 两个对象都符合这个接口,但各自的实现不同(一个返回 emp-2-linda,一个返回 mgr-1-pam)——同一结构,不同实现,这就是接口 的主要用途10。

接口还有个非常实用的场景:对接文档稀烂的第三方 API。不要图省事用 any (第 02 章说过它会传染),而是只把你关心的字段写成接口; 得益于结构类型,运行时多出来的字段会被忽略,只要你在意的字段来了, 开发期的类型安全就保住了11。注意原书的提醒:null/undefined 字段仍要 自己处理好,运行时会因此抛异常11。

接口和 type 别名的分工(书里明写的三条区别12):同名接口会自动合并; 接口惯例上用来描述「类必须实现的形状」;接口描述不了并集等非对象类型, type 可以。

4. 三种继承:类、抽象类、接口

类继承与 super

子类用 extends 继承父类;子类构造器里必须先调 super()(父类构造器) 才能碰 this13:

class Motorcycle extends Vehicle {
    constructor() { super(2); }   // 把 2 传给父类的 wheelCount
}

主线例子在 ② 站遇到了真问题:子类想提供爆胎后的更新方法,却访问不了父类的 private wheelCount——子类继承了成员,也碰不到父类的私有成员,这是封装 的刻意设计14。解法是把 private 换成 protected:类自己和所有 子类可见,外部仍不可见15。

书里顺带提了一笔 namespaces(命名空间):老式的代码分组容器,把相关的类、 函数收在一个名字下面;现代代码用 ES 模块(import/export),命名空间只剩 在 legacy 代码里认得出它即可16。

抽象类:合同和实现装进同一个壳

接口没有实现,普通类全是实现;抽象类介于两者之间——可以有一部分成员 带真实现和状态,另一部分成员只有签名,留给子类填17:

abstract class Vehicle {
    constructor(protected wheelCount: number) {}
    abstract updateWheelCount(newWheelCount: number): void;  // 只有签名
    showNumberOfWheels() { console.log(`wheels: ${this.wheelCount}`); }  // 有实现
}

标了 abstract 的类不能被实例化;子类继承了却不实现抽象成员,编译器 拒绝通过。子类还可以覆盖(override)父类的非抽象方法,换上自己的版本18。

接口继承:一个接口能同时认多个爹

接口的继承比类自由:接口可以同时 extends 多个接口(interface C extends A, B), 类也可以同时 implements 多个接口(class X implements A, B, C)19。 类实现接口时没有 super()——接口是纯编译期的东西,翻译后什么都没有, 自然没有父构造器可调;implements 只做编译期检查,运行时节口早已消失20。

接口继承的两个顺带收获

书里的 interfaceInheritance.ts 例子还有两处值得点名。其一,Motorcycle 实现 Vehicle 接口时,必须连 Thing 的成员一起实现(getFullName)——接口的 extends 是「合同合并」,子接口的签名是父接口签名加上自己的,签字画押时一并生效1。 其二,运行结果只有一行 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」——这一组是作者贯穿全章的工程口味,证据是编译器行为 本身(报错与否)加上实现层面的说明(私有字段编译后消失)6。
  • 「instanceof 满天飞是多态的反面」是明确的判断句,不是工具说明23。
  • 「工具类型先用官方的」是判断,附带理由:标准库、测试充分、广泛 使用32。

8. 边界与局限

  • override 没标 override 关键字:原书演示了覆盖但没提 TypeScript 的 override 显式标注(以及 noImplicitOverride 选项)——防「以为在覆盖、 其实父类早改名」的手段,属于本书没讲的常用进阶项。
  • 命名空间讲了两句但没上模块:所有例子都用 namespace 防重名, 原书自己承认这只是自包含演示的手段,真实代码该用 ES 模块16。
  • 这一章的 OOP 讲法非常「经典 Java 味」;在 React 函数组件的世界里, 类与继承的实际用量远小于这里的热身,真正天天见的是接口和泛型(第 05、06 章)。

判断(我们的,不是书里的): 用同一条 Vehicle/wheelCount 主线把 private → protected → abstract → interface 串起来,是这组原书节最好的设计—— 每一站都是被上一站的真实的痛逼出来的(爆胎改不了轮子数、子类不交作业、 连实现都不想要)。这比按特性字母表罗列的讲法有效得多。 如果错,会错在: 「经典 OOP 升级路线」的教学收益建立在读者接受 「继承是复用的默认答案」上;在组合优于继承成为主流口径的当下, 这条路线可能让读者高估继承在自己未来代码里的戏份。

9. 可带走的

  1. 类的成员默认 public;constructor(private x) 是声明+赋值的省写。
  2. TypeScript 的 private 编译后消失;要真私有用 JavaScript 的 #。
  3. protected = 类自己 + 子类可见;子类永远碰不到父类的 private。
  4. 接口只存在于编译期:不能 new、没有 super、implements 只是编译期检查。
  5. 接口可多重 extends、类可多重 implements——类继承没这个自由。
  6. 抽象类 = 部分实现 + 部分签名的壳;含抽象成员的类不可实例化。
  7. 多态的对偶警告:调用方写 instanceof 猜类型,就是没用好接口。
  8. 泛型是「类型的参数」;无约束的 <T> 按 unknown 对待,要用 extends 上约束。
  9. 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」)


  1. 出处:「Interface inheritance」第 159–185 段(text/18-fm-interface-inheritance.txt:167,搜「must implement both」;:179,搜「because Vehicle」)。 ↩

  2. 出处:「Classes」第 62–70 段(text/15-fm-classes.txt:62,搜「constructor」;:70,搜「initialize」)。 ↩

  3. 出处:「Classes」第 86–96 段(text/15-fm-classes.txt:94,搜「depends on how a function is called」)。 ↩

  4. 出处:「Classes」第 142–150 段(text/15-fm-classes.txt:142,搜「default accessibility」)。 ↩

  5. 出处:「Classes」第 167–175 段(text/15-fm-classes.txt:173,搜「parameter」;:175,搜「two things in one line」)。 ↩

  6. 出处:「Classes」第 294–306 段(text/15-fm-classes.txt:298,搜「only enforced at compile time」;:304,搜「truly private at runtime」)。 ↩↩↩

  7. 出处:「Classes」第 320–332 段(text/15-fm-classes.txt:328,搜「not allowed once you use」)。 ↩

  8. 出处:「Classes」第 454–470 段与 568–638 段(text/15-fm-classes.txt:452,搜「target」;:636,搜「strict defaults to true」)。 ↩

  9. 出处:「Interfaces」第 74–78 段(text/16-fm-interfaces.txt:74,搜「does not emit any JavaScript」)。 ↩

  10. 出处:「Interfaces」第 94–119 段与 169 段(text/16-fm-interfaces.txt:169,搜「different implementations」)。 ↩

  11. 出处:「Interfaces」第 187–221 段(text/16-fm-interfaces.txt:205,搜「only the fields」;:221,搜「exceptions during runtime」)。 ↩↩

  12. 出处:「Interfaces」第 27–39 段(text/16-fm-interfaces.txt:31,搜「automatically merge」)。 ↩

  13. 出处:「Interface inheritance」第 328–354 段(text/16-fm-interfaces.txt:334,搜「In a subclass constructor, you must call」)。 ↩

  14. 出处:「Interface inheritance」第 440–452 段(text/16-fm-interfaces.txt:444,搜「It's because we are trying to access a p」)。 ↩

  15. 出处:「Interface inheritance」第 482–504 段(text/16-fm-interfaces.txt:504,搜「type allows the class and any inheriting」)。 ↩

  16. 出处:「Interface inheritance」第 506–560 段(text/16-fm-interfaces.txt:550,搜「a legacy feature」)。 ↩↩

  17. 出处:「Abstract classes」第 3–11 段(text/17-fm-abstract-classes.txt:3,搜「no implementation, no state」)。 ↩

  18. 出处:「Abstract classes」第 183–203 段(text/17-fm-abstract-classes.txt:185,搜「cannot instantiate」;:203,搜「instead」)。 ↩

  19. 出处:「Interface inheritance」第 15–27 段(text/18-fm-interface-inheritance.txt:15,搜「multiple other interfaces」)。 ↩

  20. 出处:「Interface inheritance」第 141–157 段(text/18-fm-interface-inheritance.txt:147,搜「Unlike extending a class, there is no」;:155,搜「compile-time check」)。 ↩

  21. 出处:「Interface inheritance」第 203–215 段(text/18-fm-interface-inheritance.txt:207,搜「poly = many」;:213,搜「doesn't need to know」)。 ↩

  22. 出处:「Interface inheritance」第 336–372 段(text/18-fm-interface-inheritance.txt:366,搜「closed」)。 ↩

  23. 出处:「Interface inheritance」第 374–384 段(text/18-fm-interface-inheritance.txt:384,搜「opposite of polymorphism」)。 ↩↩

  24. 出处:「Interface inheritance」第 408–428 段(text/18-fm-interface-inheritance.txt:408,搜「by shape, not by name」)。 ↩

  25. 出处:「Interface inheritance」第 430–450 段(text/18-fm-interface-inheritance.txt:444,搜「chosen by the user」)。 ↩

  26. 出处:「Interface inheritance」第 539–563 段与 584–626 段(text/18-fm-interface-inheritance.txt:545,搜「is unconstrained, TypeScript treats it m」;:592,搜「T extends HasLength」)。 ↩

  27. 出处:「Interface inheritance」第 764–766 段(text/18-fm-interface-inheritance.txt:766,搜「everywhere」)。 ↩

  28. 出处:「Omit<Type, Keys>」第 57–59 段(text/21-fm-omit-type-keys.txt:59,搜「well-tested」)。 ↩

  29. 出处:「ReturnType<Type>」第 3–13 段与 48 段(text/19-fm-returntype-type.txt:5,搜「don't control directly」;:48,搜「type Result = ReturnType」)。 ↩

  30. 出处:「Pick<Type, Keys>」第 48–57 段与 85–91 段(text/20-fm-pick-type-keys.txt:89,搜「does not filter out」)。 ↩

  31. 出处:「Omit<Type, Keys>」第 3–9 段与 28–55 段(text/21-fm-omit-type-keys.txt:3,搜「removing」)。 ↩

  32. 出处:「Omit<Type, Keys>」第 61 段(text/21-fm-omit-type-keys.txt:61,搜「check the documentation」)。 ↩↩