面向 Java/C# 程序员的 TypeScript

对于习惯了 C# 和 Java 等静态类型语言的程序员来说,TypeScript 是一个热门选择。

TypeScript 的类型系统提供了许多相同的好处,例如更好的代码补全、更早地发现错误,以及程序各部分之间更清晰的沟通。虽然 TypeScript 为这些开发者提供了许多熟悉的功能,但有必要退一步看看 JavaScript(因而也是 TypeScript)与传统的面向对象语言有何不同。理解这些差异能帮助你写出更好的 JavaScript 代码,并避开那些从 C#/Java 直接转向 TypeScript 的程序员可能陷入的常见陷阱。

同时学习 JavaScript

如果你已经熟悉 JavaScript,但主要身份是 Java 或 C# 程序员,这个入门页面可以帮助解释你可能容易中招的一些常见误解和陷阱。TypeScript 对类型建模的一些方式与 Java 或 C# 相当不同,学习 TypeScript 时务必牢记这些差异。

如果你是 Java 或 C# 程序员,但对 JavaScript 整体还很陌生,我们建议先学一点不带类型的 JavaScript,以理解 JavaScript 的运行时行为。因为 TypeScript 并不改变代码运行的方式,你仍然需要了解 JavaScript 如何工作,才能写出真正能干事的代码!

务必记住,TypeScript 与 JavaScript 使用相同的运行时,因此任何关于如何实现特定运行时行为的资料(把字符串转换成数字、显示警告框、把文件写入磁盘等)都同样适用于 TypeScript 程序。不要把自己局限在 TypeScript 专属的资料里!

重新思考类

C# 和 Java 属于我们所说的强制面向对象语言。在这些语言中,类是代码组织的基本单元,也是运行时所有数据和行为的基本容器。强制把所有功能和数据都放进类中,对某些问题来说是不错的领域模型,但并不是每个领域都需要这样表达。

自由函数与数据

在 JavaScript 中,函数可以存在于任何地方,数据也可以自由传递,不必放在预定义的 class 或 struct 里。这种灵活性极其强大。“自由”函数(不与类关联的函数)作用于数据之上,没有隐含的面向对象层级,这往往是 JavaScript 中编写程序的首选模式。

静态类

此外,C# 和 Java 中的某些构造,例如单例和静态类,在 TypeScript 中并无必要。

TypeScript 中的面向对象

话虽如此,如果你愿意,仍然可以使用类!有些问题很适合用传统的面向对象层级来解决,而 TypeScript 对 JavaScript 类的支持会让这些模型更加强大。TypeScript 支持许多常见模式,例如实现接口、继承和静态方法。

我们将在本指南的后面部分介绍类。

重新思考类型

TypeScript 对类型的理解实际上与 C# 或 Java 相当不同。让我们来探讨一些差异。

名义化的具体类型系统

在 C# 或 Java 中,任何给定的值或对象都有一个确切的类型——要么是 null,要么是基元类型,要么是已知的类类型。我们可以调用 value.GetType() 或 value.getClass() 这样的方法来在运行时查询确切的类型。该类型的定义会存在于某个具有某个名称的类中,除非存在显式的继承关系或共同实现的接口,否则我们无法用两个形状相似的类互相替代。

这些方面描述的是一个具体化、名义化的类型系统。我们在代码中写下的类型在运行时是存在的,而类型之间通过它们的声明相关联,而不是通过它们的结构。

类型即集合

在 C# 或 Java 中,把运行时类型与其编译时声明看作一一对应是有意义的。

在 TypeScript 中,最好把类型看作共享某些共同点的值的集合。因为类型就是集合,所以某个特定的值可以同时属于许多个集合。

一旦你开始把类型看作集合,某些操作就会变得非常自然。例如,在 C# 中,传递一个或者是 string、或者是 int 的值很别扭,因为没有任何单一类型能表示这类值。

在 TypeScript 中,一旦你意识到每个类型都只是一个集合,这件事就变得非常自然。如何描述一个要么属于 string 集合、要么属于 number 集合的值?它只属于这两个集合的并集:string | number。

TypeScript 提供了许多以集合论方式处理类型的机制,如果你把类型看作集合,就会发现它们更加直观。

被擦除的结构化类型

在 TypeScript 中,对象并不只有单一的确切类型。例如,如果我们构造一个满足某个接口的对象,那么即使两者之间没有声明式的关系,也可以在该接口被期望的地方使用这个对象。

ts
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);
Try

TypeScript 的类型系统是结构化的,而非名义化的:我们可以把 obj 当作 Pointlike 使用,因为它具有 x 和 y 这两个都是数字的属性。类型之间的关系由它们包含的属性决定,而不是由它们是否以某种特定关系声明来决定。

TypeScript 的类型系统也不是具体化的:运行时没有任何东西会告诉我们 obj 是 Pointlike。事实上,Pointlike 类型在运行时不以任何形式存在。

回到类型即集合的想法,我们可以把 obj 看作既是 Pointlike 值集合的成员,也是 Named 值集合的成员。

结构化类型的影响

面向对象程序员常常对结构化类型的两个特定方面感到惊讶。

空类型

第一点是空类型似乎有违预期:

ts
class Empty {}
 
function fn(arg: Empty) {
// do something?
}
 
// No error, but this isn't an 'Empty' ?
fn({ k: 10 });
Try

TypeScript 通过检查所提供的实参是否是有效的 Empty 来判断这里对 fn 的调用是否有效。它通过检查 { k: 10 } 和 class Empty { } 的结构来完成这一判断。我们可以看到 { k: 10 } 具有 Empty 的全部属性,因为 Empty 没有任何属性。因此,这是一次有效的调用!

这也许看起来令人意外,但它最终与名义化面向对象语言中强制的那种关系非常相似。子类不能移除基类的属性,因为这样做会破坏派生类与其基类之间天然的子类型关系。结构化类型系统只是通过描述子类型具有类型兼容的属性,隐含地识别出这种关系。

相同的类型

另一个常见的惊讶来源是相同的类型:

ts
class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
// No error?
let w: Car = new Golfer();

同样,这并不是错误,因为这些类的结构相同。虽然这看起来可能成为混淆的来源,但在实践中,本不应相关却完全相同的类并不常见。

我们将在“类”一章中进一步了解类之间如何相互关联。

反射

面向对象程序员习惯于能够查询任何值的类型,甚至是泛型值的类型:

csharp
// C#
static void LogType<T>() {
Console.WriteLine(typeof(T).Name);
}

由于 TypeScript 的类型系统会被完全擦除,因此诸如泛型类型参数的实例化之类的信息在运行时并不可用。

JavaScript 确实有一些有限的原语,例如 typeof 和 instanceof,但请记住,这些运算符处理的仍然是类型擦除后的输出代码中存在的值。例如,typeof (new Car()) 的结果是 "object",而不是 Car 或 "Car"。

后续步骤

以上简要介绍了日常 TypeScript 开发中使用的语法和工具。接下来,你可以:

TypeScript 文档是一个开源项目。欢迎发送 Pull Request 帮助我们改进这些页面 ❤

最后更新:2026 年 10 月 23 日