TypeScript 在完全兼容 JavaScript 所有特性的基础上,额外提供了一套强大的类型系统。
例如,JavaScript 提供了诸如string和number等语言原语,但它不会检查你是否始终如一地使用这些类型。而 TypeScript 会进行这样的检查。
这意味着,现有的可运行的 JavaScript 代码,本质上也是合法的 TypeScript 代码。TypeScript 的主要优势在于,它可以帮助你发现代码中意料之外的行为,从而降低出错的可能性。
本文将简要介绍 TypeScript,并重点讲解其类型系统的核心概念,帮助你从 JavaScript 基础平稳过渡。
类型的推断
TypeScript 了解 JavaScript 的语言特性,因此在很多情况下可以自动为你推断出类型。例如,当你创建一个变量并为其赋值时,TypeScript 会根据这个值推断出对应的类型:
let helloWorld = "Hello World";
其效果等同于显式声明类型:
let helloWorld: string;
通过理解 JavaScript 的运行机制,TypeScript 构建了一套能够接受 JavaScript 代码的类型系统。这意味着你可以在不显式标注类型的情况下,依然获得完整的类型支持。上面的例子中,TypeScript 正是通过类型推断机制,知道helloWorld是一个string。
如果你曾在 Visual Studio Code 中编写过 JavaScript,并体验过其智能的自动补全功能,其底层正是依赖 TypeScript 来提升开发体验。
定义类型
在 JavaScript 中可以使用各种各样的设计模式。然而,有些模式(例如动态编程)会使自动类型推导变得困难。为了解决这些情况,TypeScript 扩展了 JavaScript 的语法,允许你显式地告诉 TypeScript 每个值的类型。
例如,创建一个可被推断为包含name: string和id: number的对象:
const user = {
name: "Hayes",
id: 0,
};
你也可以通过interface关键字来显式描述这个对象的结构:
interface User {
name: string;
id: number;
}
然后,在声明变量时使用: TypeName语法,标注该对象符合你定义的接口:
const user: User = {
name: "Hayes",
id: 0,
};
如果你传入的对象结构与接口声明不匹配,TypeScript 会发出警告:
interface User {
name: string;
id: number;
}
const user: User = {
username: "Hayes", // ❌ 报错:对象字面量只能指定已知属性,'username' 不存在于 'User' 类型中
id: 0,
};
由于 JavaScript 本身支持类和面向对象编程,TypeScript 也同样支持。你可以将interface与class结合使用:
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);
你还可以使用接口来注解函数的参数和返回值类型:
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。
组合类型
在 TypeScript 中,你可以通过组合简单类型来构建复杂类型。最常用的两种组合方式是:联合类型(Union)和泛型(Generics)。
联合类型
使用联合类型时,你可以声明某个值可以是多个类型中的任意一种。例如,可以用联合类型来精确描述布尔类型是true或false:
type MyBool = true | false;
提示:将鼠标悬停在 MyBool 上,你会发现它被识别为 boolean。这是结构类型系统的一种特性。
联合类型的一个常见用法是限定值只能是某些特定的字符串或数字字面量:
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
联合类型也可以用来处理不同类型的数据。例如,编写一个参数可以是string或string[]的函数:
function getLength(obj: string | string[]) {
return obj.length;
}
在函数内部,你可以使用typeof或Array.isArray来进行类型守卫(Type Guard),以便针对不同类型执行不同逻辑:
function wrapInArray(obj: string | string[]) {
if (typeof obj === "string") {
return [obj];
// (parameter) obj: string
}
return obj;
}
泛型
泛型为类型系统引入了“参数”的能力。一个常见的例子是数组:不带泛型的数组可以包含任意类型的值,而带泛型的数组则可以明确描述数组中元素的类型。
type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;
你也可以声明自己的泛型类型:
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// 声明一个泛型类型为 string 的 Backpack 实例
declare const backpack: Backpack<string>;
// object 是 string 类型,因为 Backpack 的泛型参数被指定为 string
const object = backpack.get();
// 由于 backpack 的泛型是 string,因此不能向 add 方法传入 number 类型的值。
backpack.add(23); // ❌ 报错:类型“number”的参数不能赋给类型“string”的参数。
结构类型系统
TypeScript 的核心原则之一是:类型检查关注的是值的“形状”或“结构”。这种方式有时也被称为鸭子类型(Duck Typing)或结构类型(Structural Typing)。
鸭子类型:如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。在 TypeScript 中,只要一个对象在结构上满足某个接口的要求,它就可以被当作该接口类型使用,而无需显式声明。
在结构类型系统中,只要两个对象具有相同的结构,它们就会被视为兼容的类型。
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// 输出 "12, 26"
const point = { x: 12, y: 26 };
logPoint(point);
变量point并没有被声明为Point类型,但在类型检查时,TypeScript 会比较point的结构与Point接口的结构。由于它们匹配,因此代码通过检查。
结构匹配并不要求完全一致,只要对象包含目标类型的所有必需属性即可:
const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // 输出 "12, 26"
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // 输出 "33, 3"
const color = { hex: "#187ABF" };
logPoint(color);
// ❌ 报错:类型“{ hex: string; }”的参数不能赋给类型“Point”的参数。
// 缺少属性 x 和 y。
类和对象在结构类型系统中的处理方式是一致的:
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); // 输出 "13, 56"
只要对象或类拥有所需的全部属性,TypeScript 就认为它们是类型兼容的,这极大地提高了 前端框架 和库之间交互的灵活性。
总结
对于有 JavaScript 背景的开发者来说,TypeScript 引入的核心新概念主要包括:
- 类型系统:支持自动推断和手动标注。
- 结构类型(鸭子类型):关注值的形状而非其声明名称,这与 Java、C# 等名义类型系统有显著区别。
- 联合类型:允许将多个类型组合成一个新类型。
- 泛型:提供可重用的类型模板,增强代码通用性和类型安全。
- 编译时擦除:类型信息仅在编译阶段用于检查,不会存在于最终的 JavaScript 运行时代码中。
掌握这些概念是高效使用 TypeScript 进行现代前端或 Node.js 后端开发的关键一步。