整个TypeScript官方文档分为这么一些模块:
快速开始
手册
参考文档
模块参考
教程
更新内容
声明文件
JavaScript
项目
原文链接:https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html
文档位置:快速开始
翻译:谢杰
审校:谢杰
TypeScript 是许多习惯使用静态类型语言(如 C# 和 Java)的程序员的热门选择。
TypeScript 的类型系统提供了许多相同的优势,例如更智能的代码补全、更早期的错误检测,以及程序各部分之间更清晰的通信。虽然 TypeScript 为这类开发者提供了大量熟悉的特性,但我们仍然有必要退一步,了解 JavaScript(也就是 TypeScript)与传统面向对象编程语言的不同之处。理解这些差异,有助于你编写更优雅的 JavaScript 代码,并避免那些从 C#/Java 直接转向 TypeScript 的开发者常见的陷阱。
共同学习 JavaScript
如果你已经熟悉 JavaScript,但主要是 Java 或 C# 开发者,这篇入门文章可以帮助你理解一些常见的误解和容易踩的坑。TypeScript 在类型建模方面的方式与 Java 或 C# 有不少不同之处,学习 TypeScript 时需要特别注意这些差异。
如果你是一位初学 JavaScript 的 Java 或 C# 程序员,我们建议你先学习一些不带类型的原生 JavaScript,以便理解 JavaScript 的运行时行为。因为 TypeScript 并不会改变代码的运行方式,你仍然需要掌握 JavaScript 的工作机制,才能编写出真正有效的代码。
务必记住,TypeScript 使用的运行时环境就是 JavaScript,因此任何关于运行时行为的学习资源(例如字符串转数字、弹出提示框、写入文件等)对 TypeScript 同样适用。不要局限于 TypeScript 专用的学习资料!
重新思考 Class
C# 和 Java 是典型的“强制面向对象编程(OOP)语言”。在这些语言中,class 是代码组织的基本单元,同时也是运行时中所有数据和行为的基本容器。将所有功能和数据都封装进 class,对于某些问题域来说是一种很好的建模方式,但并不是所有场景都需要采用这种结构。
自由的函数与数据
在 JavaScript 中,函数可以存在于任意位置,数据也可以在不依赖预定义 class 或 struct 的情况下自由传递。这种灵活性极具表现力。所谓的“自由”函数(即不属于任何 class 的函数),就是在不依赖OOP层级结构的情况下对数据进行处理,这种模式往往是 JavaScript 中更受欢迎的模式。
静态类
此外,像单例(singleton)和静态类(static class)这类来自 C# 和 Java 的结构,在 TypeScript 中并不是必需的。
TypeScript中的面向对象编程
话虽如此,如果你喜欢,依然可以使用 class!有些问题非常适合通过传统的 OOP 层级结构来解决,而 TypeScript 对 JavaScript class 的支持则让这种建模方式更加强大。TypeScript 支持许多常见的模式,例如实现接口、继承以及静态方法等。
我们会在后续的指南中介绍 class 的相关内容。
重新思考类型系统
TypeScript 对类型的理解实际上与 C# 或 Java 有很大不同。我们先来看一些关键差异。
Nominal Reified 类型系统
在 C# 或 Java 中,任意一个值或对象都有一个确切的类型。要么是 null,要么是某个原始类型,要么是某个已知的 class 类型。我们可以通过调用 value.GetType() 或 value.getClass() 等方法,在运行时查询该值的具体类型。这个类型的定义一定存在于某个命名的 class 中。而如果两个 class 拥有类似的结构,我们也无法直接互换使用它们,除非它们之间存在显式的继承关系或共同实现的接口。
这些特性构成了所谓的 reified nominal 类型系统。也就是说:我们在代码中定义的类型在运行时依然存在,并且类型之间的关系是基于声明(nominal)的,而不是基于结构的。
译者注:
Nominal Type System(名义类型系统):指类型之间的兼容性是通过“名字”来判断的,也就是说,两个类型哪怕结构完全一致,只要名字不同,就被视为不兼容。Java、C# 就是典型的 nominal 类型系统。
Reified Type System(具体化类型系统 / 实体化类型系统):指类型信息在运行时依然存在,可以被访问和检查(例如 Java 中的 getClass()、C# 中的 GetType())。这与 TypeScript、JavaScript 的类型在编译后被擦除不同。
类型即集合
在 C# 或 Java 中,运行时类型与编译时声明之间是一一对应的关系,这是有意义且成立的。
但在 TypeScript 中,更合适的思维方式是:将类型视为一组具有某些共同特性的值的集合。因为类型本质上就是集合,所以一个特定的值可以同时属于多个类型集合。
一旦你开始用“集合”的视角来看待类型,很多操作就会变得非常自然。例如,在 C# 中,传递一个值,这个值可能是 string 或 int,是相当麻烦的,因为并没有一个类型能同时表示这样的值。
而在 TypeScript 中,一旦你意识到每个类型就是一个集合,这种需求就很容易处理了。你该如何描述一个值,它可能属于 string 集合,也可能属于 number 集合?答案是:它属于这两个集合的并集:string | number。
TypeScript 提供了多种机制,用集合论的方式来处理类型。如果你用“类型是集合”的思维模式来看待这些特性,会发现它们非常直观。
被擦除的结构类型
在 TypeScript 中,对象并不具有某个唯一的确切类型。例如,当我们创建了一个对象,它满足某个接口的结构要求,即使两者之间没有声明性的关联,我们依然可以在需要该接口的地方使用这个对象。
interface Pointlike { x: number; y: number;}interface Named { name: string;}
function logPoint(point: Pointlike) { console.log("x = " + point.x + ", y = " + point.y);}
function logName(x: Named) { console.log("Hello, " + x.name);}
const obj = { x: 0, y: 0, name: "Origin",};
logPoint(obj);logName(obj);
TypeScript 的类型系统是结构化的(structural),而不是名义化的(nominal):我们之所以可以将 obj 作为 Pointlike 使用,是因为它拥有 x 和 y 两个属性,且它们的类型都是 number。类型之间的关系是由它们包含的属性决定的,而不是是否存在某种声明性的继承或实现关系。
同时,TypeScript 的类型系统也不是具体化的(reified):在运行时,我们无法获知 obj 是 Pointlike 类型。实际上,Pointlike 类型在运行时是完全不存在的。
回到“类型是集合”的思维模式,我们可以将 obj 看作同时属于 Pointlike 值集合和 Named 值集合的一个成员。
译者注:
这段文字其实在说明TypeScript使用的是鸭子类型(Duck Typing)
所谓鸭子类型,就是那句话:“如果它看起来像鸭子、叫起来像鸭子,那它就是鸭子。”
在TypeScript里,只要一个对象结构上(具有的字段和类型)满足某个接口,那它就可以被当成那个接口类型使用。即便它从来没有 implements 过这个接口。
结构类型的特点
对于习惯 OOP 的开发者来说,结构类型有两个方面常常令人意外。
空类型
第一个令人惊讶的地方是:空类型的行为可能不符合直觉。
class Empty {}
function fn(arg: Empty) { // do something?}
// 没有报错,但这不是'Empty'吧?fn({ k: 10 });
在这个例子中,TypeScript 判断是否可以调用 fn,是通过检查传入的参数是否符合 Empty 的结构要求来决定的。
它会对比 { k: 10 } 和 class Empty {} 的结构。
由于 Empty 没有任何属性,所以就相当于 { k: 10 } 包含了 Empty 所有的属性,因此它被认为是一个合法的 Empty 类型参数。这种调用在结构类型系统中是完全合法的。
这可能会让人觉得奇怪,但其实这背后的原理与名义类型语言中某些规则是类似的。在 nominal OOP 语言中,子类不能移除父类的属性,因为那样会破坏子类和父类之间自然存在的子类型关系。而在结构类型系统中,这种子类型关系是通过属性的兼容性来隐式建立的。只要一个类型具有另一个类型所有的属性(并且类型兼容),它就可以被认为是那个类型的子类型。
类型结构相同
另一个常见的困惑来源是结构相同的类型:
class Car { drive() { // hit the gas }}class Golfer { drive() { // hit the ball far }}// 没有报错?let w: Car = new Golfer();
这段代码不会报错,是因为这两个类的结构是相同的。在结构类型系统中,只要结构兼容(这里指都有一个名为 drive 的方法),就可以相互赋值。虽然这看起来可能会引发混淆,但在实际开发中,结构完全相同但语义上不应互通的类其实非常罕见。
我们将在后续的 “类” 章节中进一步学习类之间的关系。
反射
OOP 开发者通常习惯于可以查询任意值的类型,即使是泛型类型:
static void LogType<T>() { Console.WriteLine(typeof(T).Name);}
但在 TypeScript 中,由于类型系统在编译后会被完全擦除,因此像泛型类型参数的具体实例化信息,在运行时是不可获取的。
JavaScript 虽然提供了一些有限的原生操作符,比如 typeof 和 instanceof,但需要注意的是,这些操作符作用于的是类型被擦除后的实际值。例如,typeof (new Car()) 的结果是 "object",而不是 Car 或 "Car"。
译者解读:
官方的这篇文档主要介绍了TypeScript和传统的C#以及Java这类强类型语言的核心区别。
C#、Java:名义类型系统
TypeScript:结构类型系统
所谓名义类型系统,指的是类型之间的关系依赖显式的声明(如继承或实现接口),类型在运行时依然存在,可通过反射机制获取类型信息。
而结构类型系统,指的是类型之间的兼容性是由其“结构”决定的,只要结构满足要求,就认为类型兼容,无需显式的继承关系。同时,TypeScript的类型在编译后会被完全擦除,运行时无法获取任何类型信息。
这也意味着,C#/Java开发者在初学TypeScript时,最需要转变的思维在于:
类型兼容性判断方式不同:不再依赖“类名”或“继承关系”,而是关注对象是否“长得像”目标类型。
类型信息不会进入运行时:泛型参数、接口、类型别名等,仅用于编译期校验,运行时无法通过反射等方式获取类型。
函数与数据可以自由组合:TypeScript更鼓励以函数为基本构建单位,而不是强制封装在class中,风格更偏向函数式编程。
类型是集合的概念更直观:联合类型(A | B)、交叉类型(A & B)等特性让类型组合更灵活,更像是对一组值特征的集合进行建模。
整体来看,TypeScript在类型表达力上非常强大,但其类型系统的核心理念与传统 OOP 静态语言有本质差异。理解这些差异,有助于避免常见误区,更加高效地使用TypeScript进行类型建模与开发。
阅读原文:原文链接
该文章在 2026/1/4 10:40:22 编辑过