全面解析 TypeScript:一篇中文入门
引言:为什么我们需要 TypeScript?
在当今的 Web 开发世界中,JavaScript 无疑占据着核心地位。从前端的用户界面到后端的服务器应用,再到移动端甚至嵌入式设备,JavaScript 的身影无处不在。然而,随着项目规模的不断扩大和复杂度的日益提升,纯粹的 JavaScript 动态类型特性所带来的问题也逐渐显现:
- 运行时错误(Runtime Errors): JavaScript 代码中的类型错误只有在代码执行时才能被发现。这意味着即使一个小小的拼写错误或者错误的类型使用,都可能导致程序在用户使用过程中崩溃。在大型项目中,这种隐藏的错误可能潜伏很久,一旦触发,排查起来非常困难。
- 代码可维护性差: 没有明确的类型定义,代码的意图变得模糊。当你看到一个变量或函数参数时,往往不清楚它应该是什么类型,有什么属性或方法。这增加了代码阅读和理解的难度,使得团队协作和后续维护变得困难。
- 重构困难: 当需要修改代码结构或函数签名时,由于缺乏类型信息,你很难确定修改会影响到哪些地方,容易引入新的错误。
- 开发效率低下: 开发者需要花费额外的时间去猜测或查阅文档来确定变量类型和 API 用法。代码编辑器或 IDE 提供的智能提示(IntelliSense)和代码导航功能也因为缺乏精确的类型信息而大打折扣。
正是为了解决这些问题,Microsoft 在 2012 年推出了 TypeScript。TypeScript 并非一门全新的语言,它更像是 JavaScript 的一个超集。简单来说,TypeScript = JavaScript + 类型系统。所有合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 在 JavaScript 的基础上添加了可选的静态类型系统以及一些 ES6+ 的新特性(即使目标编译环境不支持)。
TypeScript 代码最终会被编译(Transpiled)成纯粹的 JavaScript 代码,这样就可以在任何支持 JavaScript 的环境中运行。
本文将带你全面深入地了解 TypeScript 的核心概念、基本用法以及它带来的巨大优势,助你迈出掌握 TypeScript 的第一步。
为什么你应该学习 TypeScript?
学习和使用 TypeScript 不仅仅是为了赶时髦,更是为了提升你的开发体验、代码质量和团队效率。以下是 TypeScript 带来的主要优势:
- 早期错误检测(Early Error Detection): TypeScript 最核心的价值在于其静态类型检查。在代码编写阶段或编译阶段,TypeScript 编译器就会检查你的代码,发现潜在的类型错误、属性访问错误等问题,而不是等到运行时才报错。这极大地减少了调试时间,提高了开发效率。
- 提升代码可读性和可维护性: 明确的类型注解让代码更具表达力。一眼就能看出变量、函数参数和返回值的预期类型,降低了理解代码的门槛。这对于新成员加入项目或老代码维护非常有帮助。
- 增强开发工具支持: 由于拥有丰富的类型信息,现代的代码编辑器和 IDE(如 VS Code、WebStorm)能够提供无与伦比的开发体验:
- 智能代码补全(IntelliSense): 当你输入变量名或对象属性时,编辑器能准确地列出可用的方法和属性。
- 类型检查和错误提示: 在编码过程中实时告诉你哪里存在类型问题。
- 代码导航和重构: 轻松跳转到变量或函数的定义处,安全地进行变量重命名、函数签名修改等重构操作。
- 提高代码健壮性: 强制的类型检查让你在编写代码时更加严谨,减少了因类型混淆导致的潜在 Bug。
- 更好的团队协作: 类型定义作为一种契约,明确了不同模块或组件之间的数据接口。团队成员可以更放心地协作,减少沟通成本和集成问题。
- 拥抱最新的 JavaScript 特性: TypeScript 支持最新的 ECMAScript 标准特性,并允许你选择编译到不同版本的 JavaScript,即使你的目标环境不支持这些新特性。
动手实践:安装与第一个 TypeScript 文件
学习编程语言最好的方法就是动手实践。让我们开始搭建环境并编写第一个 TypeScript 文件。
1. 安装 Node.js 和 npm
TypeScript 的安装和使用依赖于 Node.js 环境及其包管理器 npm(或者 yarn, pnpm)。如果你的电脑上还没有安装 Node.js,请前往 Node.js 官网 下载安装适合你操作系统的版本。安装完成后,打开终端或命令行工具,输入以下命令检查是否安装成功:
bash
node -v
npm -v
2. 安装 TypeScript
使用 npm 全局安装 TypeScript 编译器:
bash
npm install -g typescript
安装完成后,你可以通过以下命令检查 TypeScript 版本:
bash
tsc -v
tsc
是 TypeScript Compiler 的缩写,它是将 .ts
文件编译成 .js
文件的命令行工具。
3. 编写你的第一个 TypeScript 文件
创建一个新文件夹,例如 my-ts-project
,并在其中创建一个名为 hello.ts
的文件。输入以下代码:
“`typescript
// hello.ts
function greet(person: string) {
return “Hello, ” + person;
}
let user = “TypeScript Beginner”;
console.log(greet(user));
// 尝试传入错误类型
// let num = 123;
// console.log(greet(num)); // 这一行会在编译时报错
“`
在这段代码中,我们定义了一个 greet
函数,并使用 : string
为其参数 person
指定了类型——字符串。这意味着当你调用 greet
函数时,传入的参数必须是字符串类型,否则 TypeScript 编译器就会报错。
4. 编译 TypeScript 文件
打开终端或命令行工具,进入到你创建的文件夹 my-ts-project
中,然后执行 TypeScript 编译器命令:
bash
tsc hello.ts
如果你的代码没有类型错误,这个命令会生成一个名为 hello.js
的文件:
“`javascript
// hello.js (由 tsc hello.ts 生成)
function greet(person) {
return “Hello, ” + person;
}
var user = “TypeScript Beginner”;
console.log(greet(user));
// 尝试传入错误类型
// let num = 123;
// console.log(greet(num)); // 这一行会在编译时报错
“`
可以看到,生成的 JavaScript 代码就是原始 TypeScript 代码中去除类型注解的部分。现在你可以像运行普通 JavaScript 文件一样执行它:
bash
node hello.js
输出:
Hello, TypeScript Beginner
现在,如果你取消 hello.ts
中被注释掉的错误调用行(console.log(greet(num));
),再次运行 tsc hello.ts
,你会看到编译器报错:
“`
hello.ts:9:17 – error TS2345: Argument of type ‘number’ is not assignable to parameter of type ‘string’.
9 console.log(greet(num));
~~~~
“`
这个错误提示告诉你,你尝试将一个 number
类型的值赋值给一个预期为 string
类型的参数,这正是 TypeScript 提前帮你发现的类型错误!
5. 使用 tsconfig.json
对于稍大一点的项目,手动编译每个文件非常繁琐。tsconfig.json
文件是 TypeScript 项目的配置文件,它可以指定编译选项、需要编译的文件、不需要编译的文件等等。
在 my-ts-project
文件夹的根目录下创建 tsconfig.json
文件:
bash
tsc --init
这个命令会生成一个带有大量注释的默认 tsconfig.json
文件。你可以根据需要修改其中的选项。最常用的选项包括:
target
: 指定编译后的 JavaScript 版本(例如 “es5”, “es2015”, “esnext”)。module
: 指定模块系统(例如 “commonjs”, “es6”, “amd”)。strict
: 开启所有严格类型检查选项(强烈推荐)。outDir
: 指定编译输出的文件夹。rootDir
: 指定 TypeScript 源文件的根目录。
例如,一个简单的 tsconfig.json
可能看起来像这样:
json
// tsconfig.json
{
"compilerOptions": {
"target": "es2015", // 编译到 ES2015 (ES6) 版本的 JS
"module": "commonjs", // 使用 CommonJS 模块系统
"strict": true, // 启用所有严格类型检查
"outDir": "./dist", // 将编译后的 JS 文件输出到 dist 文件夹
"rootDir": "./src" // 假设你的 TS 源文件在 src 文件夹中
},
"include": [
"src/**/*.ts" // 包含 src 文件夹下所有 .ts 文件
]
}
有了 tsconfig.json
文件后,你只需要在项目根目录下运行 tsc
命令,编译器就会查找并使用该文件进行项目编译。
TypeScript 核心概念:类型系统详解
现在,让我们深入了解 TypeScript 的核心——类型系统。
1. 基本类型 (Basic Types)
TypeScript 支持所有 JavaScript 已有的基本类型,并在此基础上增加了一些自己的类型。
- 布尔类型 (boolean): 表示真/假值。
typescript
let isDone: boolean = false; - 数字类型 (number): 支持整数和浮点数。
typescript
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744; - 字符串类型 (string): 表示文本数据。
typescript
let color: string = "blue";
let fullName: string = `Bob Adams`; // 支持模板字符串 - 数组类型 (Array): 表示同类型元素的有序集合。有两种定义方式。
typescript
let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3]; // 使用泛型数组类型 - 元组类型 (Tuple): 表示一个已知元素数量和类型的数组,各元素的类型不必相同。
typescript
// 定义一对 string 和 number 类型的元组
let x: [string, number];
// 初始化
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error
console.log(x[0].substring(1)); // OK
// console.log(x[1].substring(1)); // Error, number 没有 substring 方法 -
枚举类型 (Enum): 为一组数值赋予友好的名字。默认从 0 开始为元素编号。
“`typescript
enum Color {Red, Green, Blue}
let c: Color = Color.Green; // c 的值为 1enum Color {Red = 1, Green, Blue} // 可以手动指定起始编号
let c2: Color = Color.Green; // c2 的值为 2enum Color {Red = 1, Green = 2, Blue = 4} // 可以手动指定所有值
let c3: Color = Color.Green; // c3 的值为 2// 可以通过值查找对应的名字
let colorName: string = Color[2]; // colorName 的值为 “Green”
* **Any 类型 (any):** 表示可以是任何类型。当你不知道变量类型,或者希望允许变量在运行时改变类型时使用。**应尽量避免使用 `any`,因为它会绕过 TypeScript 的类型检查。**
typescript
let notSure: any = 4;
notSure = “maybe a string instead”;
notSure = false; // ok, definitely a boolean
当你声明一个变量但没有指定类型,也没有初始化时,它会被默认为 `any` 类型。
typescript
* **Void 类型 (void):** 表示没有任何类型。常用作函数没有返回值时的返回类型。
function warnUser(): void {
console.log(“This is a warning message”);
}
let unusable: void = undefined; // 在 strictNullChecks 未启用时,可以赋值 undefined 或 null
// let unusable: void = null; // 同样
* **Null 和 Undefined 类型 (null, undefined):** 默认情况下,`null` 和 `undefined` 是所有类型的子类型,你可以将 `null` 或 `undefined` 赋值给 `number`、`string` 等类型的变量。然而,当启用 `strictNullChecks` 编译选项时(强烈推荐),它们只能赋值给 `void` 或自身类型。
typescript
let u: undefined = undefined;
let n: null = null;// let num: number = null; // Error 当 strictNullChecks 启用时
* **Never 类型 (never):** 表示永不存在的值的类型。例如,总是抛出异常或根本不会有返回值的函数表达式或箭头函数表达式的返回值类型。
typescript
// 返回 never 的函数必须存在无法到达的终点
function error(message: string): never {
throw new Error(message);
}// 推断的返回值类型为 never
function fail() {
return error(“Something failed”);
}// 返回 never 的函数也可能在它的内部循环里永远不会结束
function infiniteLoop(): never {
while (true) {
}
}
* **Object 类型 (object):** 表示非原始类型,即除 `number`, `string`, `boolean`, `symbol`, `null`, `undefined` 之外的类型。
typescript
declare function create(o: object | null): void;create({ prop: 0 }); // OK
create(null); // OK// create(42); // Error
// create(“string”); // Error
// create(false); // Error
// create(undefined); // Error
* **Unknown 类型 (unknown):** 表示未知类型。它是类型安全的 `any`。当你不知道一个值的类型时,可以使用 `unknown`。与 `any` 不同的是,在使用 `unknown` 类型的值之前,你必须先进行类型检查或类型断言,否则不能对其进行任何操作(除了等号判断或 typeof、instanceof 判断)。
typescript
let value: unknown;value = “hello”;
value = 123;// let value1: string = value; // Error: Type ‘unknown’ is not assignable to type ‘string’.
// let value2: any = value; // OK// 必须进行类型检查才能使用
if (typeof value === ‘string’) {
console.log(value.length); // OK, value is narrowed to string
}// 或者使用类型断言 (不推荐,除非确定类型)
console.log((value as string).length); // OK (运行时可能出错)
“`
2. 接口 (Interfaces)
接口是 TypeScript 中非常重要的概念,它用于定义对象的结构(形状)。接口可以定义对象的属性、方法,以及可选属性、只读属性等。
“`typescript
interface Person {
firstName: string;
lastName: string;
age?: number; // 可选属性
readonly hobby: string; // 只读属性
greet(): void; // 方法签名
}
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
let myObj = { size: 10, label: “Size 10 Object” };
printLabel(myObj); // OK, myObj 至少包含 label 属性
// 使用接口
interface LabelledValue {
label: string;
}
function printLabelWithInterface(labeledObj: LabelledValue) {
console.log(labeledObj.label);
}
printLabelWithInterface(myObj); // OK
// 定义一个符合 Person 接口的对象
let user: Person = {
firstName: “Jane”,
lastName: “Doe”,
// age 是可选的,可以不写
hobby: “reading”,
greet: function() {
console.log(Hello, my name is ${this.firstName} ${this.lastName}.
);
}
};
user.greet();
// user.hobby = “sports”; // Error: Cannot assign to ‘hobby’ because it is a read-only property.
“`
接口不仅可以描述普通对象的形状,还可以描述函数类型、类、以及构造函数。
3. 类型别名 (Type Aliases)
类型别名用来为现有类型创建一个新的名字。类型别名可以用来定义基本类型、联合类型、交叉类型、元组、接口等的别名。
“`typescript
// 为基本类型起别名
type MyString = string;
let str: MyString = “hello”;
// 为联合类型起别名
type StringOrNumber = string | number; // 联合类型,表示可以是 string 或 number
let value: StringOrNumber = “abc”;
value = 123;
// value = true; // Error
// 为交叉类型起别名
type Countable = { count: number };
type Nameable = { name: string };
type NamedCountable = Countable & Nameable; // 交叉类型,表示同时拥有 count 和 name 属性
let item: NamedCountable = { count: 10, name: “widgets” };
// 为对象字面量起别名 (与接口类似,但有一些区别)
type Point = {
x: number;
y: number;
};
function printCoordinate(pt: Point) {
console.log(“The coordinate’s x value is ” + pt.x);
console.log(“The coordinate’s y value is ” + pt.y);
}
printCoordinate({ x: 3, y: 7 });
// 为函数类型起别名
type GreetFunction = (name: string) => string;
const myGreet: GreetFunction = (name) => Hello, ${name}!
;
console.log(myGreet(“TypeScript”));
“`
接口 (Interface) vs. 类型别名 (Type Alias):
- 扩展性 (Extensibility): 接口可以多次声明,同名的接口会自动合并(声明合并)。接口可以使用
extends
来继承其他接口。类型别名不能被继承,也不能声明合并。 - 实现 (Implements): 类可以实现(
implements
)接口,但不能实现类型别名定义的联合类型或交叉类型。类可以实现类型别名定义的对象类型。 - 适用范围: 接口主要用于定义对象的形状和类的契约。类型别名可以为任何类型(包括基本类型、联合/交叉类型、函数类型等)起别名。
- 社区偏好: 对于定义对象的形状或 API 契约,通常更倾向于使用接口,因为它提供了更好的可扩展性。对于其他情况,如联合类型、交叉类型等,必须使用类型别名。
在实际开发中,你可以选择一种风格并在项目中保持一致。通常,对于库或公共 API 的类型定义,使用接口更常见。对于内部使用的类型组合,类型别名也很方便。
4. 函数类型 (Function Types)
函数在 JavaScript 和 TypeScript 中都是一等公民。TypeScript 可以非常详细地描述函数的类型。
“`typescript
// 为函数参数和返回值指定类型
function add(x: number, y: number): number {
return x + y;
}
let result = add(5, 3); // result 的类型被推断为 number
// 也可以为整个函数变量指定类型(使用类型别名或接口)
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
// 可选参数 (Optional Parameters): 在参数名后加 ?
function buildName(firstName: string, lastName?: string): string {
if (lastName)
return firstName + ” ” + lastName;
else
return firstName;
}
let result1 = buildName(“Bob”); // OK
// let result2 = buildName(“Bob”, “Adams”, “Sr.”); // Error, 参数过多
let result3 = buildName(“Bob”, “Adams”); // OK
// 默认参数 (Default Parameters): 在参数后使用 =
赋值
function buildNameWithDefault(firstName: string, lastName: string = “Smith”): string {
return firstName + ” ” + lastName;
}
let result4 = buildNameWithDefault(“Bob”); // 等同于 buildNameWithDefault(“Bob”, “Smith”)
let result5 = buildNameWithDefault(“Bob”, “Adams”); // 等同于 buildNameWithDefault(“Bob”, “Adams”)
// 剩余参数 (Rest Parameters): 使用 ...
语法将多个参数收集到一个数组中
function sum(a: number, b: number, …restOfNumbers: number[]): number {
let total = a + b;
for (let i = 0; i < restOfNumbers.length; i++) {
total += restOfNumbers[i];
}
return total;
}
let total = sum(1, 2, 3, 4, 5); // restOfNumbers 会是 [3, 4, 5]
// 函数重载 (Function Overloads): 为同一个函数提供多个函数类型定义,根据参数数量或类型选择正确的签名
function multiply(x: number, y: number): number; // 重载签名 1
function multiply(x: string, y: string): string; // 重载签名 2
function multiply(x: any, y: any): any { // 实现签名 (不能直接被外部调用,只用于内部实现)
if (typeof x === ‘number’ && typeof y === ‘number’) {
return x * y;
} else if (typeof x === ‘string’ && typeof y === ‘string’) {
return x + y; // 或者其他字符串操作
}
throw new Error(“Invalid input types”);
}
let prod = multiply(2, 3); // 匹配第一个签名,prod 是 number
let concat = multiply(“hello”, “world”); // 匹配第二个签名,concat 是 string
// let invalid = multiply(2, “world”); // Error, 不匹配任何重载签名
“`
5. 类 (Classes)
TypeScript 对 ES6 的类提供了完整的支持,并增加了诸如访问修饰符、抽象类等特性。
“`typescript
class Animal {
// 默认是 public
name: string;
// public 是默认的,可以省略
public species: string;
// private: 只能在类内部访问
private age: number;
// protected: 可以在类内部和子类中访问
protected moveSpeed: number = 5;
// 只读属性
readonly origin: string = “earth”;
constructor(name: string, species: string, age: number) {
this.name = name;
this.species = species;
this.age = age;
}
move(distanceInMeters: number = 0) {
console.log(${this.name} moved ${distanceInMeters}m.
);
}
// private 方法
private introduce() {
console.log(I am ${this.name}, a ${this.species}. I am ${this.age} years old.
);
}
// public 方法调用 private 方法
public showInfo() {
this.introduce();
// console.log(this.age); // OK
}
}
let cat = new Animal(“Kitty”, “Felis catus”, 3);
cat.move(10);
cat.showInfo();
// console.log(cat.age); // Error: Property ‘age’ is private and only accessible within class ‘Animal’.
// console.log(cat.introduce()); // Error: Property ‘introduce’ is private…
// 继承 (Inheritance)
class Dog extends Animal {
breed: string;
constructor(name: string, age: number, breed: string) {
super(name, “Canis lupus familiaris”, age); // 调用父类的 constructor
this.breed = breed;
}
bark() {
console.log(“Woof! Woof!”);
}
// override 父类的方法
move(distanceInMeters = 5) {
console.log(${this.name} is running...
);
// console.log(My speed is ${this.moveSpeed}
); // OK, protected 属性可以在子类中访问
super.move(distanceInMeters); // 调用父类的 move 方法
}
}
let dog = new Dog(“Buddy”, 5, “Golden Retriever”);
dog.move();
dog.bark();
// console.log(dog.moveSpeed); // Error: Property ‘moveSpeed’ is protected and only accessible within class ‘Animal’ and its subclasses.
// 实现接口 (Implementing Interfaces)
interface Swimmable {
swim(): void;
}
class Fish extends Animal implements Swimmable { // 一个类可以实现一个或多个接口
constructor(name: string, age: number) {
super(name, “Fish”, age);
}
swim() {
console.log(`${this.name} is swimming.`);
}
}
let goldfish = new Fish(“Nemo”, 1);
goldfish.swim();
“`
6. 泛型 (Generics)
泛型是 TypeScript 中实现代码复用和类型安全的关键特性。它允许你编写能够处理多种类型的组件,而不仅仅是单一类型。泛型的主要目的是在保证类型安全的前提下,让代码更灵活、更通用。
考虑一个简单的函数,它接收一个参数并返回该参数:
“`typescript
function identity(arg: number): number {
return arg;
}
function identity(arg: string): string {
return arg;
}
// 问题:每次都需要为不同的类型写一个函数
“`
使用 any
可以处理多种类型,但丢失了类型信息:
“`typescript
function identityAny(arg: any): any {
return arg;
}
let outputAny = identityAny(“myString”); // outputAny 是 any 类型,我们不知道它是一个字符串
// console.log(outputAny.length); // OK,但运行时可能出错如果传入的是数字
“`
泛型解决了这个问题。我们使用一个类型变量 <T>
(或者其他字母,如 <U>
, <K>
, <V>
等,T 是 Type 的缩写,是约定俗成的写法)来表示类型:
“`typescript
// 使用泛型
function identity
return arg;
}
// 调用时,可以明确指定 T 的类型 (通常不推荐,编译器会自动推断)
let output1 = identity
console.log(output1.length); // OK, 编译器知道 output1 是 string 类型
// 调用时,让编译器自动推断 T 的类型 (更常用)
let output2 = identity(“myString”); // T 被推断为 string
console.log(output2.length); // OK
let output3 = identity(123); // T 被推断为 number
// console.log(output3.length); // Error, 编译器知道 output3 是 number 类型,number 没有 length 属性
“`
泛型不仅用于函数,也可以用于接口、类、类型别名等。
泛型接口:
“`typescript
interface GenericIdentityFn
(arg: T): T;
}
function identity
return arg;
}
let myIdentity: GenericIdentityFn
console.log(myIdentity(10)); // OK, myIdentity 现在是 number 版本的 identity
// console.log(myIdentity(“hello”)); // Error
“`
泛型类:
“`typescript
class GenericNumber
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, add: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = add;
}
}
let myGenericNumber = new GenericNumber
console.log(myGenericNumber.add(5, 6)); // 11
let myGenericString = new GenericNumber
console.log(myGenericString.add(“hello”, “world”)); // “helloworld”
“`
泛型约束 (Generic Constraints): 有时你希望泛型参数具有某些特定的属性或方法。可以使用 extends
关键字来约束泛型参数的类型。
“`typescript
interface Lengthwise {
length: number;
}
// T 必须满足 Lengthwise 接口,即必须有一个 number 类型的 length 属性
function loggingIdentity
console.log(arg.length); // OK, 现在我们知道 T 类型的值肯定有 .length 属性
return arg;
}
// loggingIdentity(3); // Error, number 没有 length 属性
loggingIdentity({ length: 10, value: 3 }); // OK
“`
泛型是 TypeScript 中比较高级但非常实用的特性,它能让你的代码更加灵活和安全。
7. 类型断言 (Type Assertions)
类型断言用来告诉编译器“我知道这个变量的类型,请相信我”。它不是类型转换,也不会影响运行时代码,只是在编译阶段起作用。当你比编译器更清楚某个变量的具体类型时,可以使用类型断言。
有两种形式:
- “尖括号”语法 (Angle-bracket syntax):
<Type>value
typescript
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length; as
语法 (as-syntax):value as Type
typescript
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
在 JSX 中,<Type>value
语法可能会与 JSX 语法冲突,因此通常推荐使用as
语法。
注意: 类型断言很强大,但也伴随着风险。如果你断言的类型不正确,运行时可能会出现错误。只在你百分之百确定变量类型时使用类型断言。
8. 类型守卫与类型缩小 (Type Guards and Type Narrowing)
类型守卫是一种表达式,它执行运行时检查,以确保在某个作用域内的变量是某种特定的类型。 TypeScript 利用这些守卫来缩小变量的类型范围,这个过程称为类型缩小 (Type Narrowing)。
常见的类型守卫包括:
typeof
守卫: 检查变量的基本类型 ('string'
,'number'
,'boolean'
,'undefined'
,'object'
,'function'
,'symbol'
,'bigint'
)。
typescript
function printLength(x: string | number) {
if (typeof x === "string") {
console.log(x.length); // 在这个 if 块里,x 被缩小为 string 类型
} else {
console.log(x.toFixed(2)); // 在这个 else 块里,x 被缩小为 number 类型
}
}-
instanceof
守卫: 检查变量是否是某个类的实例。
“`typescript
class Dog { bark() {} }
class Cat { meow() {} }function petSays(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // 在这个 if 块里,pet 被缩小为 Dog 类型
} else {
pet.meow(); // 在这个 else 块里,pet 被缩小为 Cat 类型
}
}
* **`in` 守卫:** 检查对象是否包含某个属性。
typescript
interface Fish { swim: () => void; }
interface Bird { fly: () => void; }function isFish(pet: Fish | Bird): pet is Fish { // 这是自定义类型守卫,后面会讲
return (pet as Fish).swim !== undefined;
}function move(pet: Fish | Bird) {
if (“swim” in pet) {
pet.swim(); // 在这个 if 块里,pet 被缩小为 Fish 类型 (如果 swim 是 Fish 独有的属性)
} else {
pet.fly(); // 在这个 else 块里,pet 被缩小为 Bird 类型
}
}
* **等值(`==`, `===`, `!=`, `!==`)和非等值收窄:** 检查变量与 `null`, `undefined` 或特定常量的值。
typescript
function processMaybeString(s: string | null | undefined) {
if (s !== null && s !== undefined) { // 排除 null 和 undefined
console.log(s.length); // 在这里,s 被缩小为 string
}
}
* **自定义类型守卫 (User-Defined Type Guards):** 你可以定义一个函数,其返回值是 `parameterName is Type` 这种形式,来告诉编译器这个函数是类型守卫。
typescript
interface Fish { swim: () => void; }
interface Bird { fly: () => void; }// pet is Fish 是一个类型谓词 (type predicate),表示如果函数返回 true,那么 pet 就是 Fish 类型
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as any).swim !== undefined;
}function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // pet 的类型被缩小为 Fish
} else {
pet.fly(); // pet 的类型被缩小为 Bird
}
}
“`
类型守卫和类型缩小是 TypeScript 中非常强大的特性,它们使得在处理联合类型时能够进行类型安全的操作。
TypeScript 与 JavaScript 生态系统
TypeScript 与现有的 JavaScript 生态系统兼容性非常好:
- 集成现有库: 大多数流行的 JavaScript 库(如 React, Vue, Angular, Lodash, jQuery)都提供了 TypeScript 类型定义文件(通常以
@types/library-name
的形式发布到 npm)。安装这些类型定义后,你就可以在 TypeScript 项目中享受到对这些库的类型检查和智能提示了。
bash
npm install react @types/react react-dom @types/react-dom
npm install lodash @types/lodash - 构建工具: Webpack, Rollup, Parcel, Vite 等现代构建工具都提供了对 TypeScript 的良好支持,通常只需要简单的配置即可集成
tsc
或ts-loader
等插件。 - 框架支持: Angular 完全使用 TypeScript 构建。React 和 Vue 项目也广泛使用 TypeScript,社区提供了丰富的 TypeScript 模板和最佳实践。
- 与 Babel 共存: Babel 也可以用来编译 TypeScript 代码(通过
@babel/preset-typescript
)。这种方式编译速度可能更快,但类型检查仍然由tsc
或其他工具负责。
编写高质量 TypeScript 代码的建议
- 启用严格模式 (
strict: true
): 这是tsconfig.json
中最重要的选项之一。它会开启诸如noImplicitAny
,strictNullChecks
,strictFunctionTypes
等一系列严格检查,帮助你编写更安全、更规范的代码。 - 避免滥用
any
: 尽量为变量和函数参数/返回值指定具体的类型。只有在你确实不知道或不关心类型时才使用any
,但更好的选择通常是unknown
。 - 善用接口和类型别名: 清晰地定义你的数据结构和函数签名,提高代码的可读性和可维护性。
- 利用类型推断: TypeScript 编译器很聪明,在很多情况下可以自动推断出变量的类型。你不需要在所有地方都写类型注解,只需要在公共 API (函数参数/返回值, 接口, 类属性等) 处明确标注即可。
typescript
let x = "hello"; // x 被推断为 string,无需 `: string` - 理解类型守卫: 在处理联合类型时,正确使用类型守卫可以确保代码的类型安全性。
- 配置好
tsconfig.json
: 根据项目需求合理配置编译选项,确保代码质量和兼容性。
结论
TypeScript 为 JavaScript 带来了强大的静态类型系统,有效解决了 JavaScript 在大型应用开发中面临的可维护性、可读性和 Bug 率高等问题。通过类型检查、丰富的类型定义能力以及对现代 JavaScript 特性的支持,TypeScript 极大地提升了开发效率和代码质量。
从基础类型、接口、类型别名,到函数、类、泛型以及类型守卫,本文为你勾勒出了 TypeScript 的核心蓝图。这仅仅是一个开始,TypeScript 还有很多高级特性等待你去探索,比如装饰器、命名空间、模块解析策略、条件类型、映射类型等等。
迈出学习 TypeScript 的第一步,你将发现它不仅仅是一种语言,更是一种提升开发体验和构建健壮、可维护应用的强大工具。现在就开始在你的新项目中使用 TypeScript,或者尝试将现有 JavaScript 项目逐步迁移到 TypeScript 吧!祝你在 TypeScript 的世界里编码愉快!