面向 Java/C# 程序员的 TypeScript

对于习惯于使用其他静态类型语言(例如 C# 和 Java)的程序员来说,TypeScript 是一个流行的选择。

¥TypeScript is a popular choice for programmers accustomed to other languages with static typing, such as C# and Java.

TypeScript 的类型系统提供了许多相同的好处,例如更好的代码完成、更早的错误检测以及程序各部分之间更清晰的通信。虽然 TypeScript 为这些开发者提供了许多熟悉的功能,但值得回头看看 JavaScript(以及 TypeScript)与传统 OOP 语言有何不同。了解这些差异将帮助你编写更好的 JavaScript 代码,并避免直接从 C#/Java 转到 TypeScript 的程序员可能掉入的常见陷阱。

¥TypeScript’s type system offers many of the same benefits, such as better code completion, earlier detection of errors, and clearer communication between parts of your program. While TypeScript provides many familiar features for these developers, it’s worth stepping back to see how JavaScript (and therefore TypeScript) differ from traditional OOP languages. Understanding these differences will help you write better JavaScript code, and avoid common pitfalls that programmers who go straight from C#/Java to TypeScript may fall in to.

共同学习 JavaScript

¥Co-learning JavaScript

如果你已经熟悉 JavaScript 但主要是 Java 或 C# 程序员,则此介绍性页面可以帮助解释你可能容易陷入的一些常见误解和陷阱。TypeScript 建模类型的一些方式与 Java 或 C# 完全不同,在学习 TypeScript 时记住这些很重要。

¥If you’re familiar with JavaScript already but are primarily a Java or C# programmer, this introductory page can help explain some of the common misconceptions and pitfalls you might be susceptible to. Some of the ways that TypeScript models types are quite different from Java or C#, and it’s important to keep these in mind when learning TypeScript.

如果你是 Java 或 C# 程序员,一般来说是 JavaScript 的新手,我们建议你先学习一些没有类型的 JavaScript,以了解 JavaScript 的运行时行为。因为 TypeScript 不会改变代码的运行方式,所以你仍然必须了解 JavaScript 的工作原理才能编写真正执行某些操作的代码!

¥If you’re a Java or C# programmer that is new to JavaScript in general, we recommend learning a little bit of JavaScript without types first to understand JavaScript’s runtime behaviors. Because TypeScript doesn’t change how your code runs, you’ll still have to learn how JavaScript works in order to write code that actually does something!

重要的是要记住 TypeScript 使用与 JavaScript 相同的运行时,因此关于如何完成特定运行时行为(将字符串转换为数字、显示警报、将文件写入磁盘等)的任何资源都将始终同样适用于 TypeScript 程序。不要将自己局限于特定于 TypeScript 的资源!

¥It’s important to remember that TypeScript uses the same runtime as JavaScript, so any resources about how to accomplish specific runtime behavior (converting a string to a number, displaying an alert, writing a file to disk, etc.) will always apply equally well to TypeScript programs. Don’t limit yourself to TypeScript-specific resources!

反思类

¥Rethinking the Class

C# 和 Java 是我们所谓的强制性 OOP 语言。在这些语言中,类是代码组织的基本单位,也是运行时所有数据和行为的基本容器。强制将所有功能和数据保存在类中可能是解决某些问题的良好字段模型,但并非每个字段都需要以这种方式表示。

¥C# and Java are what we might call mandatory OOP languages. In these languages, the class is the basic unit of code organization, and also the basic container of all data and behavior at runtime. Forcing all functionality and data to be held in classes can be a good domain model for some problems, but not every domain needs to be represented this way.

释放函数和数据

¥Free Functions and Data

在 JavaScript 中,函数可以存在于任何地方,数据可以自由传递,而无需在预定义的 classstruct 中。这种灵活性非常强大。“自由的” 函数(与类无关的函数)在没有隐含的 OOP 层次结构的情况下处理数据往往是用 JavaScript 编写程序的首选模型。

¥In JavaScript, functions can live anywhere, and data can be passed around freely without being inside a pre-defined class or struct. This flexibility is extremely powerful. “Free” functions (those not associated with a class) working over data without an implied OOP hierarchy tends to be the preferred model for writing programs in JavaScript.

静态类

¥Static Classes

此外,TypeScript 中不需要来自 C# 和 Java 的某些构造,例如单例和静态类。

¥Additionally, certain constructs from C# and Java such as singletons and static classes are unnecessary in TypeScript.

TypeScript 中的 OOP

¥OOP in TypeScript

也就是说,如果你愿意,你仍然可以使用类!有些问题很适合通过传统的 OOP 层次结构来解决,而 TypeScript 对 JavaScript 类的支持将使这些模型更加强大。TypeScript 支持许多常见模式,例如实现接口、继承和静态方法。

¥That said, you can still use classes if you like! Some problems are well-suited to being solved by a traditional OOP hierarchy, and TypeScript’s support for JavaScript classes will make these models even more powerful. TypeScript supports many common patterns such as implementing interfaces, inheritance, and static methods.

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

¥We’ll cover classes later in this guide.

反思类型

¥Rethinking Types

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

¥TypeScript’s understanding of a type is actually quite different from C# or Java’s. Let’s explore some differences.

名义具体化的类型系统

¥Nominal Reified Type Systems

在 C# 或 Java 中,任何给定值或对象都有一种确切的类型 - null、基础类型或已知类类型。我们可以调用 value.GetType()value.getClass() 之类的方法来在运行时查询确切的类型。这种类型的定义将驻留在某个具有某个名称的类中,我们不能使用两个具有相似形状的类来代替彼此,除非存在显式继承关系或共同实现的接口。

¥In C# or Java, any given value or object has one exact type - either null, a primitive, or a known class type. We can call methods like value.GetType() or value.getClass() to query the exact type at runtime. The definition of this type will reside in a class somewhere with some name, and we can’t use two classes with similar shapes in lieu of each other unless there’s an explicit inheritance relationship or commonly-implemented interface.

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

¥These aspects describe a reified, nominal type system. The types we wrote in the code are present at runtime, and the types are related via their declarations, not their structures.

作为集合的类型

¥Types as Sets

在 C# 或 Java 中,考虑运行时类型与其编译时声明之间的一对一对应关系是有意义的。

¥In C# or Java, it’s meaningful to think of a one-to-one correspondence between runtime types and their compile-time declarations.

在 TypeScript 中,最好将类型视为一组具有共同点的值。因为类型只是集合,所以一个特定的值可以同时属于多个集合。

¥In TypeScript, it’s better to think of a type as a set of values that share something in common. Because types are just sets, a particular value can belong to many sets at the same time.

一旦开始将类型视为集合,某些操作就会变得非常自然。例如,在 C# 中,传递 stringint 值是很尴尬的,因为没有一种类型可以表示这种值。

¥Once you start thinking of types as sets, certain operations become very natural. For example, in C#, it’s awkward to pass around a value that is either a string or int, because there isn’t a single type that represents this sort of value.

在 TypeScript 中,一旦你意识到每个类型只是一个集合,这就变得很自然了。你如何描述属于 string 集合或 number 集合的值?它只是属于这些集合的并集:string | number

¥In TypeScript, this becomes very natural once you realize that every type is just a set. How do you describe a value that either belongs in the string set or the number set? It simply belongs to the union of those sets: string | number.

TypeScript 提供了许多机制来以集合论的方式处理类型,如果你将类型视为集合,你会发现它们更直观。

¥TypeScript provides a number of mechanisms to work with types in a set-theoretic way, and you’ll find them more intuitive if you think of types as sets.

擦除的结构类型

¥Erased Structural Types

在 TypeScript 中,对象不是单一的精确类型。例如,如果我们构造一个满足接口的对象,我们可以在需要该接口的地方使用该对象,即使两者之间没有声明关系。

¥In TypeScript, objects are not of a single exact type. For example, if we construct an object that satisfies an interface, we can use that object where that interface is expected even though there was no declarative relationship between the two.

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,因为它具有 xy 属性,它们都是数字。类型之间的关系取决于它们包含的属性,而不是它们是否被声明为具有某种特定关系。

¥TypeScript’s type system is structural, not nominal: We can use obj as a Pointlike because it has x and y properties that are both numbers. The relationships between types are determined by the properties they contain, not whether they were declared with some particular relationship.

TypeScript 的类型系统也没有具体化:运行时没有任何信息可以告诉我们 objPointlike。事实上,Pointlike 类型在运行时不会以任何形式出现。

¥TypeScript’s type system is also not reified: There’s nothing at runtime that will tell us that obj is Pointlike. In fact, the Pointlike type is not present in any form at runtime.

回到类型作为集合的想法,我们可以将 obj 视为 Pointlike 值集和 Named 值集的成员。

¥Going back to the idea of types as sets, we can think of obj as being a member of both the Pointlike set of values and the Named set of values.

结构类型的后果

¥Consequences of Structural Typing

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

¥OOP programmers are often surprised by two particular aspects of structural typing.

空类型

¥Empty Types

首先是空类型似乎出乎意料:

¥The first is that the empty type seems to defy expectation:

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 没有任何属性。因此,这是一个有效的调用!

¥TypeScript determines if the call to fn here is valid by seeing if the provided argument is a valid Empty. It does so by examining the structure of { k: 10 } and class Empty { }. We can see that { k: 10 } has all of the properties that Empty does, because Empty has no properties. Therefore, this is a valid call!

这可能看起来令人惊讶,但它最终与名义 OOP 语言中强制执行的关系非常相似。子类不能删除其基类的属性,因为这样做会破坏派生类与其基类之间的自然子类型关系。结构类型系统通过根据具有兼容类型的属性来描述子类型来简单地隐式识别这种关系。

¥This may seem surprising, but it’s ultimately a very similar relationship to one enforced in nominal OOP languages. A subclass cannot remove a property of its base class, because doing so would destroy the natural subtype relationship between the derived class and its base. Structural type systems simply identify this relationship implicitly by describing subtypes in terms of having properties of compatible types.

相同类型

¥Identical Types

另一个经常出人意料的来源是相同的类型:

¥Another frequent source of surprise comes with identical types:

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

同样,这不是错误,因为这些类的结构是相同的。虽然这看起来像是一个潜在的混淆来源,但在实践中,不应该相关的相同类并不常见。

¥Again, this isn’t an error because the structures of these classes are the same. While this may seem like a potential source of confusion, in practice, identical classes that shouldn’t be related are not common.

我们将在“类”一章中详细了解类之间的关系。

¥We’ll learn more about how classes relate to each other in the Classes chapter.

反射

¥Reflection

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

¥OOP programmers are accustomed to being able to query the type of any value, even a generic one:

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

由于 TypeScript 的类型系统已被完全擦除,因此有关例如泛型类型参数的实例化在运行时不可用。

¥Because TypeScript’s type system is fully erased, information about e.g. the instantiation of a generic type parameter is not available at runtime.

JavaScript 确实有一些有限的基础类型,如 typeofinstanceof,但请记住,这些运算符仍在处理类型擦除输出代码中存在的值。例如,typeof (new Car()) 将是 "object",而不是 Car"Car"

¥JavaScript does have some limited primitives like typeof and instanceof, but remember that these operators are still working on the values as they exist in the type-erased output code. For example, typeof (new Car()) will be "object", not Car or "Car".

下一步

¥Next Steps

这是对日常 TypeScript 中使用的语法和工具的简要概述。从这里,你可以:

¥This was a brief overview of the syntax and tools used in everyday TypeScript. From here, you can: