Typescript 乃 JavaScript 子集。只要设置一下编译器为非严格模式,那么所有 JavaScript 代码皆是合法的 TypeScript 代码。为了可以适应不同的场景 TypeScript 尽可能做到非常灵活。本文中,我们将会深入了解 TypeScript 中的类型兼容性,并尝试解释什么是结构类型系统(Structure Type System)。
TypeScript is a superset of JavaScript. Any JavaScript code is a valid TypeScript code, as long we set the compiler not to be strict. Therefore, TypeScript aims to be as flexible as possible so that it can apply to various situations. In this article, we look into type compatibility in TypeScript and explain what a structural type system is.

相当于的语言属于“标明类型系统(Nominal Type System)”。下面的例子说明,只有两个变量是同一类型的才是兼容的。我们来看看 C# 代码。
There are a lot of languages with a nominal type system. The above means that the two variables are compatible if they are of the same type. Let’s examine this C# code:

public class Employee
{
    public string name;
    public Employee(string name)
    {
        this.name = name;
    }
}

public class Person
{
    public string name;
    public Person(string name)
    {
        this.name = name;
    }
}

Employee john = new Employee('John');
john = new Person('John');

显然以上 C# 代码有错误。在标明类型系统下,Employee 与 Person 并不等价兼容的。类似地,像 Java 或 C++ 那些语言中也是同样的情况。
The above C# code causes an error. Due to a nominal type system, Employee and Person are not compatible. Similarly, such situations occur in languages like Java and C++.

这种好处是能让我们减少类型不匹配的错误,但就是不太灵活。TypeScript 中的结构类型系统则给我们带来更多的自由程度。
The above behavior might help us to prevent mismatching types, but it is not very flexible. To give us more freedom, TypeScript implements a structural type system.

结构类型系统 Structural type system

在结构类型系统的语言中,两种类型是否等价是通过结构来决定的,而不是名称。下面的 TypeScript 写起来,其实很像我们之前写 JavaScript 那样子的写法。
In a language with a structural type system, two types are compatible judging by their structure, instead of the name. The above allows TypeScript to adjust to the way that we often write the JavaScript code.

type Employee = {
  name: string;
}
 
class Person {
  public constructor (readonly name: string) {}
}
 
const john: Employee = new Person('John');

Person 类的构造器里面因为有 readonly 关键字,所以就会分配一个 name 的属性。
Above, we simplify the assignment of properties in the constructor of the Person class with the use of the readonly keyword.

TypeScript 中上述代码完全合法。更进一步说,即使不完全相同的类型,只要结构一致我们都可以去使用。
In TypeScript, the above code is perfectly valid. Going even further, we can use types that are not identical when it comes to its structure.

结构化子类型 Structural subtyping

要使得一种类型等价于其他类型,至少要一个相同的属性。
For one type to be compatible with the other, it needs to have at least the same properties.

interface Employee {
  name: string;
  workplaceType: string;
}
 
interface Person {
  name: string;
}
 
function printName(person: Person) {
  console.log(person.name);
}

由于 Employee 已经有 Person 的所有属性,所以下面给出 printName 函数可以传入 Employee
Above, we have the printName function. Since the Employee has all the properties of a Person.

const john: Employee = {
  name: 'John',
  workplaceType: 'Music store'
}
 
printName(john);

但反过来,要让 Employee 等价于 Person,却是不行的。
The fact that the Person is compatible with the Employee does not mean that it works the other way around.

function printWorkplace(person: Employee) {
  console.log(person.name);
}

const john: Person = {
  name: 'John',
}
 
printWorkplace(john);

传参 “Person” 没分配到 “Employee” 的参数类型。 “Person” 类型中没有 “workplace”属性,而在“Employee” 是必须的。
Argument of type ‘Person’ is not assignable to parameter of type ‘Employee’. Property ‘workplaceType’ is missing in type ‘Person’ but required in type ‘Employee’.

错误的原因就是 Person 没有 Employee 所有的属性。
The above happens because the Person does not have all the properties of the Employee.

当 Employee 继承 Person 时候也是差不多这个意思,这是子类型的情形。
Similar subtyping happens when the Employee extends the Person.

interface Person {
  name: string;
}
 
interface Employee extends Person {
  workplaceType: string;
}

在标记式子类型语言中,实现等价兼容的唯一方式就是子类型。好在 TypeScript 是结构化类型语言,不然的话,“accidental” subtypes work issue-free.(抱歉 不会翻译)。
In languages with nominal subtyping, it would be the only way to achieve compatible subtypes. Thanks to TypeScript being a structurally typed language, instead, “accidental” subtypes work issue-free.

多态 Polymorphism

printName(person: Person)函数传参数 Employee,这种就是多态。Employee 拥有 person 所有的属性,故此可以多态,包括其实例。
Calling the printName(person: Person) function using the Employee is an example of polymorphism. Since we know that the employee has all the properties of a person, we can treat its instance as such.

最简单直观的还是型状的例子,计算其面积。
The most straightforward way to visualize it is with the use of an example with shapes and calculating their areas.

interface Shape {
  getArea(): number;
}
 
class Circle {
  constructor(readonly radius: number) {}
  getArea() {
    return Math.pow(this.radius, 2) * Math.PI;
  }
}
 
class Square {
  constructor(readonly size: number) {}
  getArea() {
    return Math.pow(this.size, 2);
  }
}

虽然 Circle 或 Square 都没有继承 Shape,但是都有 getArea 函数。所有只要我们需要,都可以把圆形或者方形当作图形型状。
Although neither Circle nor Square extends Shape explicitly, all of them have the getArea function. If that’s all we need, we can treat Circle and Square as a Shape.

const shapes = [
  new Circle(10),
  new Square(5),
  new Circle(2),
  new Square(25)
]
 
const sortedShapes = shapes.sort((firstShape: Shape, secondShape: Shape) => (
  firstShape.getArea() - secondShape.getArea()
))

区分类型守卫 Differentiating types with Type Guards

不过,一般都没那么简单的类型。有时我们面对的是联合或未知类型。好在 TypeScript 已经考虑到这一点,我们做起来没那么辛苦。好比说在获取 API 的异步数据,假设是获取 user 并打印其 workplace(当类型是 Employee 的时候)。
We don’t always have such straightforward types. Sometimes, we need to deal with unions and the unknown. Thankfully, TypeScript has mechanisms to help us with that. Such a situation might occur when we fetch the data from various APIs. Let’s say we want to fetch a user and print his workplace type if he is an employee.

function printWorkplaceType(employee: Employee) {
  console.log(employee.workplaceType);
}

type FetchUser = () => Promise<unknown>;

fetchUser()
  .then((user) => {
    printWorkplaceType(user);
  })

坏消息是上面的代码跑不通的,抛出下面的错误。
Unfortunately, the above does not work. We experience an error:

传参 “unknown” 没分配到 “Employee” 的参数类型。 “{}” 类型中没有 “name”、“workplace”属性,而在 “Employee” 是必须的。
Argument of type ‘unknown’ is not assignable to parameter of type ‘Employee’. Type ‘{}’ is missing the following properties from type ‘Employee’: name, workplaceType

原因是我们不能保证获取的就有 Employee 属性。乍看之下我们好像可以先检查一下 workplaceType 属性是否存在。
This is because we are not sure if what we fetch is a proper employee. At first glance, we might want to check the existence of the workplaceType property.

fetchUser()
  .then(user => {
    if (user.workplaceType) {
      printWorkplaceType(user);
    }
  })

上面代码也是不行的。unknow 类型并没有一个workplaceType属性。哪怕我们检查这个属性但是编译器并不认为它是 Employee 的属性。
The above does not work either, because Property ‘workplaceType’ does not exist on type ‘unknown’. Even if we could check this property, the compiler wouldn’t treat it as a proper Employee.

我们也不能使用 instanceof 操作符,这是因为 Employee 是一个接口的缘故。针对该问题,不是没有办法,办法就是“类型守卫”。
We also can’t use the instanceof operator, because the Employee is just an interface. A solution to this issue are Type Guards.

定义类型守卫 Defining Type Guards

类型守卫就是在运行时保证某个类型的函数。
A type guard is a function that guarantees a type during a runtime check.

function isEmployee(user: any): user is Employee {
  return Boolean(user.workplaceType && user.name);
}

user is Employee 意思是类型预测。类型预测是特殊的返回类型表示返回特定的类型是什么。
The user is Employee is a type predicate. Type predicates are a special return type that signals the type of a particular value.

现在我们就可以在逻辑中轻松实现,保证返回的 user 是 Employee 类型。
Now, we can easily implement it in our logic to make sure that what we fetch is a proper Employee.

fetchUser()
  .then(user => {
    if (isEmployee(user)) {
      printWorkplaceType(user);
    }
  })

上述代码中只要检查了类型那么我们就可以安全地调用 printWorkplaceType 函数。
In the above code, we check the type in the runtime so that we can safely call the printWorkplaceType function.

我们可以使用 in 操作符,会更清晰一点。
We can make our code a bit cleaner using the in operator.

in 操作符的作用是判断对象或其原型链上是否存在某个指定的属性 The in operator returns true if the
specified property is in the specified object or its prototype chain.

function isEmployee(user: any): user is Employee {
  return 'workplaceType' in user && 'name' in user;
}

总结 Summary

本文中我们考察了两种类型系统:结构化的和标记式的。我们也了解 TypeScript 使用结构化类型系统的原因和理由,而且明白了,TypeScript 中的多态是什么,以及如何去应用多态,那都不需要明确扩展接口就可以。在某些地方我们用到了类型守卫和类型预测,还有 in 操作符。
In this article, we’ve reviewed two types of systems: structural and nominal. We’ve also looked through the consequences and reasons of TypeScript having the structural type system. We explained what polymorphism is and how we can apply it to TypeScript without extending interfaces explicitly. To help us in some situations, we’ve used type guards with type predicates and the in operator.

参考: