TypeScript与JavaScript之间的关系颇为特殊。TypeScript不仅具备JavaScript的所有功能,还额外提供了类型系统这一重要特性。
举例来说,JavaScript虽然提供了诸如string字符串、number数字之类的语言基础元素,但它并不会检查开发者对这些元素的赋值操作是否符合规范。而TypeScript则可以完成此类校验工作。
正因如此,原本能够正常运行的JavaScript代码同样也是合法的TypeScript代码。TypeScript的核心优势在于它能够及时检测出代码中可能存在的异常情况,从而有效降低程序出现Bug的概率。
本文介绍TypeScript的相关知识,重点阐述其类型系统的运作机制。
类型推断
TypeScript 对 JavaScript 语言有深入的了解,在许多情况下都能自动生成对应的类型。例如,当创建变量并为其赋值时,TypeScript 会以该值作为变量的类型。
通过深入了解JavaScript的运行机制,TypeScript得以构建出一套既能兼容JavaScript代码又能为其赋予类型信息的类型系统。这样一来,用户无需在代码中添加任何额外字符来显式声明类型即可实现类型检查功能。正因如此,在前示例中TypeScript才能准确判定"helloWorld"属于字符串类型。
相信不少开发者都曾在Visual Studio Code中编写过JavaScript代码并体验过其自动补全功能。实际上,Visual Studio Code正是借助TypeScript来实现对JavaScript代码的便捷处理。
定义类型
在 JavaScript 中,你可以使用多种设计模式。然而,某些设计模式会导致类型难以被自动推断(例如那些涉及动态编程的模式)。为解决这一问题,TypeScript 对 JavaScript 语言进行了扩展,提供了专门的位置供开发者向 TypeScript 明确指定各变量的类型。
举例来说,若要创建一个其类型被自动推断为包含 name: string 与 id: number 属性的对象,可编写如下代码:

你可以通过interface声明来明确描述该对象的形状:

你可以在变量声明后使用类似 :TypeName的语法,从而声明某个 JavaScript 对象符合新定义的interface结构。

如果您提供的对象不符合所定义的interface规范,TypeScript会发出警告:

由于 JavaScript 支持类与面向对象编程,TypeScript 同样具备这一功能。你可以将接口声明与类配合使用:
ts
interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
const user: User = new UserAccount("Murphy", 1);
你可以使用接口来为函数的参数及返回值添加注解:
ts
function deleteUser(user: User) {
// ...
}
function getAdminUser(): User {
//...
}
JavaScript中已经内置了一些基础类型:boolean、bigint、null、number、string、symbol以及undefined,这些类型均可用于接口定义。TypeScript在此基础上又新增了几种类型,包括any(可接受任意类型的值)、unknown(要求使用该类型的开发者明确指定具体类型)、never(表示不可能出现该类型的值)以及void(用于表示函数不返回任何值或返回值为undefined)。
在类型定义方面,TypeScript提供了两种语法形式:interface与type。通常情况下应优先选用interface;仅在需要借助type的特定功能时才使用type。
类型组合
在 TypeScript 中,可以通过组合简单类型来创建复杂类型。实现这一目的的常见方式有两种:联合类型与泛型。
联合类型
使用联合类型,可以声明某个变量的类型可以是多种类型中的任意一种。例如,可以将boolean布尔类型定义为只能取 true 或 false 的值。
ts
type MyBool = true | false;
注意 :如果将鼠标悬停在上面的
MyBool上,会发现它被归类为boolean布尔类型。这是结构化类型系统的一个特性。关于这一点我们稍后会详细讨论。
联合类型的常见用途之一是描述某个值所允许使用的字符串或数值字面量集合:
ts
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
联合体同样也可用于处理不同类型的数据。例如,你可能有一个函数需要接收数组或字符串作为参数:
ts
function getLength(obj: string | string[]) {
return obj.length;
}
若要获取变量的类型信息,可使用 typeof:

例如,你可以编写一个函数,使其根据传入的参数是字符串还是数组来返回不同的值。
泛型
泛型机制允许为类型定义变量。一个常见的例子就是数组。未使用泛型的数组可以存储任意类型的数据;而使用了泛型的数组则能够明确限定其所能容纳的数据类型。
ts
type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;
你可以声明使用泛型的自定义类型:
ts
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// 这一行代码是一种快捷方式,用于告知 TypeScript:
// 存在一个名为 `backpack` 的常量,无需关心其来源。
declare const backpack: Backpack<string>;
// 该对象是一个string字符串,因为我们之前已将其声明为 Backpack 类中的变量部分。
const object = backpack.get();
// 由于 backpack 变量是string字符串类型,因此无法向 add 函数传入number数字参数。
// backpack.add(23); //错误!

结构化类型系统
TypeScript的核心原则之一便是:类型检查主要依据值的结构形态来进行。这种做法有时也被称作 "鸭子类型 (duck typing)" 或 "结构化类型(structural typing)" 。
在结构化类型系统中,只要两个对象具有相同的结构形态,它们便被视为同一类型。
ts
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// logs "12, 26"
const point = { x: 12, y: 26 };
logPoint(point); //类型形态相同 (既鸭子类型)
虽然 point 变量从未被声明为 Point 类型,但在类型检查时,TypeScript 会对 point 对象的结构形态与 Point 类型的定义进行比较。由于两者的结构形态完全一致,因此该代码能够顺利通过编译。
这种结构匹配机制仅要求对象中部分字段的属性值相符即可。
ts
const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // logs "12, 26"
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // logs "33, 3"
const color = { hex: "#187ABF" };
//logPoint(color); //错误!

类与对象遵循形状规则的方式并无任何区别:
ts
class VirtualPoint {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const newVPoint = new VirtualPoint(13, 56);
logPoint(newVPoint); // logs "13, 56"
只要对象或类具备所有必需的属性,TypeScript便会判定二者相匹配,而无需考虑具体的实现细节。