TypeScript 教程:从零开始学
迈向更健壮、更可维护的 JavaScript 世界
引言
前端开发的世界日新月异,JavaScript 作为其基石,以其灵活性和易上手性赢得了全球开发者的青睐。然而,随着项目规模的扩大和复杂度的增加,JavaScript 的动态类型特性也带来了挑战:潜在的运行时错误难以在开发阶段被发现,代码的可读性和可维护性下降,大型团队协作时更容易出错。
正是在这样的背景下,TypeScript 应运而生。
TypeScript 是什么?
简单来说,TypeScript 是 JavaScript 的一个超集。这意味着任何合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 在 JavaScript 的基础上增加了静态类型系统以及其他面向对象特性(如接口、抽象类等)。TypeScript 代码需要通过编译器转换(或称“编译”)成纯粹的 JavaScript 代码,才能在浏览器或 Node.js 环境中运行。
为什么学习 TypeScript?
学习 TypeScript 的好处不胜枚举,尤其对于希望提升开发效率、构建大型应用或进行团队协作的开发者而言:
- 提前发现错误 (Early Error Detection): 类型检查在编译阶段进行,而不是等到运行时。这能捕获大量的潜在错误,比如拼写错误的函数名、使用了错误类型的数据等,大大减少调试时间。
- 增强代码可读性与可维护性: 类型注解清晰地表明了变量、函数参数、返回值的预期类型,使得代码意图更加明确,更易于理解和修改。
- 提升开发效率与体验:
- 强大的编辑器支持: VS Code、WebStorm 等现代编辑器对 TypeScript 有着出色的支持,提供智能代码补全、类型提示、重构、跳转到定义等功能,极大地提升了开发效率。
- 重构更安全: 在大型项目中进行代码重构时,类型系统可以帮助你确保改动不会破坏其他部分的逻辑。
- 更好的团队协作: 明确的类型定义构成了契约,使得团队成员之间更容易理解和集成彼此的代码。
- 拥抱最新的 ECMAScript 特性: TypeScript 通常会支持最新的 ECMAScript 标准特性,即使你的目标运行环境(如旧版浏览器)不支持,TypeScript 编译器也能将其转换成兼容的代码。
如果你已经具备 JavaScript 基础,并且希望提升自己的开发技能,构建更稳定、更易于维护的应用,那么学习 TypeScript 是一个非常值得的投资。
本文将带你从零开始,一步步深入了解 TypeScript 的核心概念和用法。让我们开始这段学习之旅吧!
第一章:准备工作与第一个 TypeScript 文件
开始学习之前,我们需要搭建开发环境。
- 安装 Node.js: TypeScript 编译器
tsc
是一个 Node.js 包。请确保你的系统中已经安装了 Node.js(建议选择 LTS 版本)。可以访问 Node.js 官网 (https://nodejs.org/) 下载安装包。安装完成后,打开终端或命令行工具,输入node -v
和npm -v
(或者yarn -v
如果你使用 Yarn 包管理器),检查是否安装成功。 - 安装 TypeScript: 打开终端或命令行工具,使用 npm 或 Yarn 全局安装 TypeScript:
bash
npm install -g typescript
# 或者使用 yarn
# yarn global add typescript
安装完成后,输入tsc -v
检查 TypeScript 编译器是否安装成功并显示版本号。
编写第一个 TypeScript 文件
现在,我们来创建一个简单的 TypeScript 文件。
- 在你喜欢的位置创建一个新文件夹,比如
my-ts-project
。 - 在该文件夹内创建一个名为
hello.ts
的文件(注意后缀是.ts
)。 -
在
hello.ts
文件中输入以下代码:“`typescript
function greet(person: string) {
return “Hello, ” + person;
}let user = “TypeScript User”;
console.log(greet(user));
// 下面这行代码会导致类型错误,但我们可以先写上看看效果
// let num: number = greet(user); // 尝试将 string 赋给 number
“`注意
person: string
和let num: number
中的: string
和: number
,这就是 TypeScript 的类型注解。
编译 TypeScript 代码
TypeScript 代码不能直接在浏览器或 Node.js 中运行,需要先编译成 JavaScript。
- 打开终端或命令行工具,进入到
my-ts-project
文件夹。 -
运行 TypeScript 编译器命令:
bash
tsc hello.ts -
如果代码没有类型错误,该命令会在同一目录下生成一个
hello.js
文件。打开hello.js
,你会看到类似这样的内容:javascript
function greet(person) {
return "Hello, " + person;
}
var user = "TypeScript User";
console.log(greet(user));
// 下面这行代码会导致类型错误,但我们可以先写上看看效果
// let num = greet(user); // 尝试将 string 赋给 number
可以看到,类型注解在编译后的 JavaScript 代码中被移除了。 -
处理类型错误: 如果你取消注释
let num: number = greet(user);
这行代码,再次运行tsc hello.ts
,你会看到编译器报告一个错误:
“`
hello.ts:7:5 – error TS2322: Type ‘string’ is not assignable to type ‘number’.7 let num: number = greet(user); // 尝试将 string 赋给 number
~~~Found 1 error in the latest compilation.
``
string
这就是 TypeScript 在编译阶段捕获的类型错误!它明确告诉你不能将一个类型的值赋给一个
number` 类型的变量。
运行编译后的 JavaScript 文件
现在,我们可以运行生成的 hello.js
文件:
bash
node hello.js
你将在终端看到输出:Hello, TypeScript User
。
使用 tsconfig.json 配置项目
对于稍微复杂的项目,手动编译每个 .ts
文件会非常繁琐。更常见的方式是使用 tsconfig.json
文件来配置 TypeScript 项目。
-
在
my-ts-project
文件夹的根目录下,运行以下命令生成一个默认的tsconfig.json
文件:bash
tsc --init -
该命令会生成一个包含许多配置选项的
tsconfig.json
文件。这个文件告诉 TypeScript 编译器如何编译整个项目。你可以根据需要修改这些选项。例如,你可以找到并修改target
(指定编译目标 JavaScript 版本,如 “es5”, “es2015”, “es2020” 等)和outDir
(指定编译输出目录)等选项。json
// tsconfig.json 示例 (部分)
{
"compilerOptions": {
"target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', 'ES2022', 'ESNext'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', 'ESNext'. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
"rootDir": "./", /* Specify the root folder within your source files. */
// ... 还有很多其他选项
},
// 指定需要编译的文件或文件夹
"include": [
"./*.ts"
// "./src/**/*.ts" // 例如,如果你的源文件在 src 目录下
]
} -
有了
tsconfig.json
文件后,只需在项目根目录运行tsc
命令(不带文件名),编译器就会查找并使用tsconfig.json
中的配置来编译整个项目。bash
tsc
如果配置了outDir: "./dist"
,编译后的hello.js
将会被输出到dist
文件夹中。 -
监听模式: 在开发过程中,你可能希望在修改
.ts
文件后自动进行编译。可以使用tsc --watch
命令:bash
tsc --watch
这将启动一个进程,监听项目文件的变化,并在文件保存时自动重新编译。
到此,你已经成功搭建了 TypeScript 环境,并编译运行了第一个 TypeScript 文件。接下来,我们将深入学习 TypeScript 的核心——类型系统。
第二章:TypeScript 核心:基础类型
TypeScript 的强大之处在于其静态类型系统。理解并正确使用各种类型是掌握 TypeScript 的关键。
类型注解 (Type Annotations)
类型注解用来显式地指定变量、函数参数、函数返回值等的类型。其语法是 : type
。
“`typescript
let age: number; // 声明一个 number 类型的变量
age = 30;
// age = “thirty”; // 错误:不能将 string 赋给 number
let name: string = “Alice”; // 声明并初始化一个 string 类型的变量
let isDone: boolean = false; // 声明并初始化一个 boolean 类型的变量
function greet(person: string): string { // 函数参数 person 是 string 类型,返回值是 string 类型
return “Hello, ” + person;
}
“`
类型推断 (Type Inference)
在很多情况下,TypeScript 编译器可以根据上下文自动推断出变量的类型,无需显式注解。这使得代码更加简洁。
“`typescript
let age = 30; // TypeScript 推断 age 为 number 类型
// age = “thirty”; // 错误:age 已经被推断为 number
let name = “Alice”; // TypeScript 推断 name 为 string 类型
let isDone = false; // TypeScript 推断 isDone 为 boolean 类型
// 函数返回值类型也可以被推断
function greet(person: string) {
return “Hello, ” + person; // TypeScript 推断返回值类型为 string
}
“`
尽管有类型推断,但在以下情况建议使用类型注解:
* 声明变量时没有立即赋值。
* 函数参数(因为没有初始值供推断)。
* 函数返回值(尤其当你希望明确返回值类型时)。
原始类型 (Primitive Types)
TypeScript 支持 JavaScript 的所有原始类型:
-
number
: 表示数字,包括整数和浮点数。typescript
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744; -
string
: 表示文本数据。typescript
let color: string = "blue";
color = 'red'; // 单引号双引号都可以
let fullName: string = `Bob Adams`; // 模板字符串
let greeting: string = `Hello, my name is ${fullName}.`; -
boolean
: 表示布尔值,true
或false
。typescript
let isApproved: boolean = true; -
null
和undefined
: 分别表示null
和undefined
值。默认情况下,null
和undefined
是所有类型的子类型,可以赋值给任何类型。然而,在启用strictNullChecks
选项时,它们只能赋值给null
或undefined
类型自身,或者any
类型(以及它们的联合类型),这能有效防止常见的空引用错误。“`typescript
let n: null = null;
let u: undefined = undefined;// 当 strictNullChecks: false 时
// let s: string = null; // 可以
// let num: number = undefined; // 可以// 当 strictNullChecks: true 时
// let s: string = null; // 错误
// let num: number = undefined; // 错误
let sOrNull: string | null = null; // 可以
``
tsconfig.json
强烈建议在中开启
strictNullChecks` 选项,以提高代码健壮性。 -
symbol
: 表示全局唯一引用。typescript
const sym1 = Symbol('key');
const sym2 = Symbol('key');
// sym1 === sym2 // false -
bigint
: 表示任意精度的整数。需要目标环境支持(如 ES2020 及以上)。typescript
let bigIntVar: bigint = 100n;
// let numberVar: number = bigIntVar; // 错误:不能将 bigint 赋给 number
特殊类型
-
any
: 表示任意类型。当你不确定一个变量的类型,或者希望允许其在后续被赋予任何类型的值时,可以使用any
。使用any
会放弃类型检查,回到 JavaScript 的动态类型模式。应谨慎使用any
,因为它失去了 TypeScript 的主要优势。“`typescript
let notSure: any = 4;
notSure = “maybe a string instead”;
notSure = false; // ok, 它可以是任何类型let list: any[] = [1, true, “free”];
list[1] = 100; // ok
“` -
unknown
: 表示未知类型。与any
类似,unknown
类型的变量可以被赋予任何类型的值。但是,与any
不同的是,你不能直接对unknown
类型的变量进行任意操作(比如调用方法或访问属性),除非你先进行类型检查或类型断言,证明其是某个具体的类型。这使得unknown
比any
更安全。“`typescript
let value: unknown;value = “hello”;
value = 123;// let s: string = value; // 错误:unknown 不能直接赋给 string
// value.toFixed(2); // 错误:unknown 不能直接调用方法// 需要进行类型检查
if (typeof value === ‘string’) {
let s: string = value; // ok,在 if 块中 value 被缩小为 string 类型
console.log(value.toUpperCase()); // ok
}
“` -
void
: 通常用于表示函数没有返回值。“`typescript
function warnUser(): void {
console.log(“This is my warning message”);
}let unusable: void = undefined; // 在 strictNullChecks: false 时,可以赋值 undefined
// let unusable: void = null; // 在 strictNullChecks: false 时,可以赋值 null
// 在 strictNullChecks: true 时,只能赋值 undefined 给 void
“` -
never
: 表示永远不会发生的类型。例如,一个总是抛出异常的函数,或者一个无限循环的函数的返回值类型就是never
。never
是任何类型的子类型,但没有任何类型是never
的子类型(除了never
自身)。它常用于穷举检查。“`typescript
// 总是抛出异常
function error(message: string): never {
throw new Error(message);
}// 总是执行不完
function infiniteLoop(): never {
while (true) {
}
}// 用于穷举检查
declare function process(x: string | number): void;
function controlFlowAnalysisWithNever(x: string | number) {
if (typeof x === ‘string’) {
process(x);
} else if (typeof x === ‘number’) {
process(x);
} else {
// 如果 x 是 never 类型,说明前面的 if/else if 已经覆盖了所有可能的类型
// 如果未来有人给 x 添加了新的联合类型(比如 boolean),而这里没有处理,
// 那么 else 块中的 unhandled 就会被赋给 boolean 类型,导致编译错误,从而提醒我们处理新的类型。
const unhandled: never = x;
}
}
“`
掌握了这些基础类型,你就迈出了 TypeScript 学习的重要一步。接下来,我们将看看如何组合这些基础类型来描述更复杂的数据结构。
第三章:更复杂的类型结构
除了原始类型,TypeScript 还提供了描述集合和结构化数据的方式。
数组 (Arrays)
表示同类型元素组成的列表。有两种定义方式:
-
元素类型后面接
[]
:typescript
let list: number[] = [1, 2, 3];
// list.push("4"); // 错误:不能添加 string -
使用泛型
Array<元素类型>
:typescript
let list: Array<number> = [1, 2, 3];
元组 (Tuples)
元组表示一个已知元素数量和类型的数组,各元素的类型可以不同。顺序和数量必须匹配。
“`typescript
// 声明一个元组,第一个元素是 string,第二个元素是 number
let x: [string, number];
x = [“hello”, 10]; // 正确
// x = [10, “hello”]; // 错误:类型顺序不匹配
// x = [“hello”, 10, “world”]; // 错误:元素数量不匹配
console.log(x[0].substring(1)); // ok
// console.log(x[1].substring(1)); // 错误:number 类型没有 substring 方法
// 越界访问元组元素 (在 strict: true 下可能报错,或返回联合类型)
// x[3] = “world”; // 错误:元组长度是固定的
// 然而,可以对已知位置的元素进行类型正确的新赋值
x[0] = “world”; // ok
x[1] = 20; // ok
“`
枚举 (Enums)
enum
类型用于定义一组命名的常量集合。它可以让代码更易读,特别是对于一组相关的选项。
默认情况下,枚举成员会被赋值从 0 开始递增的数字。
“`typescript
enum Color {
Red, // 0
Green, // 1
Blue // 2
}
let c: Color = Color.Green;
console.log(c); // 输出 1
“`
你也可以手动为枚举成员赋值:
“`typescript
enum Color {
Red = 1,
Green = 2,
Blue = 4
}
let c: Color = Color.Green;
console.log(c); // 输出 2
“`
甚至可以全部手动赋值:
“`typescript
enum Status {
Success = “SUCCESS”,
Failure = “FAILURE”,
Pending = “PENDING”
}
let s: Status = Status.Success;
console.log(s); // 输出 “SUCCESS”
“`
枚举还支持反向映射(仅限数字枚举):可以通过枚举的值获取其名字。
“`typescript
enum Color {
Red,
Green,
Blue
}
let colorName: string = Color[1];
console.log(colorName); // 输出 “Green”
“`
对象类型 (Object Types)
除了原始类型,JavaScript 中的大多数东西都是对象。TypeScript 使用对象字面量 ({}
) 或接口 (interface
) 来描述对象的结构。这里先介绍对象字面量表示法。
你可以通过列出对象的属性及其类型来定义对象类型:
“`typescript
let point: { x: number; y: number };
point = { x: 10, y: 20 }; // 正确
// point = { x: 10 }; // 错误:缺少属性 y
// point = { x: 10, y: “20” }; // 错误:属性 y 类型不正确
let person: { name: string; age: number; isStudent?: boolean }; // isStudent 是可选属性
person = { name: “Bob”, age: 25 }; // 正确,可选属性可以省略
person = { name: “Bob”, age: 25, isStudent: true }; // 正确
“`
可选属性使用 ?
标记。
Type vs Interface: 虽然对象字面量可以直接定义类型,但对于复杂的或需要复用的对象结构,通常使用 interface
或 type
别名(将在后续章节介绍)来定义,这样更清晰且易于管理。
至此,我们了解了如何使用基础类型和一些基本的结构类型来描述数据。接下来,我们将看看如何在函数中使用类型。
第四章:函数 (Functions)
函数是 JavaScript 和 TypeScript 中执行任务的基本单元。TypeScript 允许你对函数的参数和返回值进行类型注解,从而提高代码的健壮性。
函数类型注解
你可以为函数参数添加类型注解,以及为函数的返回值添加类型注解。
“`typescript
// 为参数和返回值添加类型注解
function add(x: number, y: number): number {
return x + y;
}
let result = add(5, 3); // result 会被推断为 number 类型
// 错误:参数类型不匹配
// add(“hello”, 3);
// 错误:返回值类型不匹配 (虽然这里 add 的返回值是 number,如果函数体实现有问题,返回值类型注解可以帮你发现)
// function add(x: number, y: number): string {
// return x + y; // 错误:number + number 结果是 number,不能赋给 string
// }
// 对于没有返回值的函数,可以使用 void
function logMessage(message: string): void {
console.log(message);
}
“`
函数表达式的类型
你也可以为整个函数表达式定义类型。
“`typescript
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
// 函数类型签名 (x: number, y: number) => number 表示一个函数,它接受两个 number 类型的参数,并返回一个 number 类型的值。
// 注意:类型签名中的参数名可以与函数实现中的参数名不同,但类型必须匹配。
let myMultiply: (a: number, b: number) => number =
function(x: number, y: number): number { return x * y; };
“`
可选参数和默认参数
在 JavaScript 中,函数的参数是可选的。在 TypeScript 中,你可以使用 ?
来标记可选参数。可选参数必须放在必选参数的后面。
“`typescript
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + ” ” + lastName;
} else {
return firstName;
}
}
let result1 = buildName(“Bob”); // 正确
// let result2 = buildName(“Bob”, “Adams”, “Sr.”); // 错误:参数过多
let result3 = buildName(“Bob”, “Adams”); // 正确
“`
你也可以为参数设置默认值。设置了默认值的参数,如果没有传递值,则会使用默认值。带有默认值的参数可以放在必选参数的前面或后面,但如果放在前面,调用时必须显式传递 undefined
来使用默认值。通常建议将带默认值的参数放在必选参数后面。
“`typescript
function buildNameWithDefault(firstName: string, lastName = “Smith”): string {
return firstName + ” ” + lastName;
}
let result4 = buildNameWithDefault(“Bob”); // 正确,使用默认值 “Smith”,输出 “Bob Smith”
let result5 = buildNameWithDefault(“Bob”, undefined); // 正确,使用默认值 “Smith”,输出 “Bob Smith”
let result6 = buildNameWithDefault(“Bob”, “Adams”); // 正确,输出 “Bob Adams”
“`
剩余参数 (Rest Parameters)
当你不知道函数会有多少个同类型的参数时,可以使用剩余参数,它会将这些参数收集到一个数组中。
“`typescript
function sumAll(firstNumber: number, …restOfNumbers: number[]): number {
let total = firstNumber;
for (let i = 0; i < restOfNumbers.length; i++) {
total += restOfNumbers[i];
}
return total;
}
let totalSum = sumAll(1, 2, 3, 4, 5); // 第一个参数是 1,剩余参数是 [2, 3, 4, 5]
console.log(totalSum); // 输出 15
“`
函数重载 (Function Overloading)
函数重载允许你为同一个函数提供多个类型签名,但函数实现只有一个。这在函数根据参数的不同类型或数量执行不同的操作时很有用。
“`typescript
// 重载签名 (不包含函数体)
function add(x: string, y: string): string;
function add(x: number, y: number): number;
function add(x: number[], y: number[]): number[];
// 函数实现 (使用联合类型或 any,必须兼容所有重载签名)
// 注意:函数实现本身的签名不能被外部直接调用,它只是为了提供实际的逻辑
function add(x: any, y: any): any {
if (Array.isArray(x) && Array.isArray(y)) {
return […x, …y]; // 数组拼接
} else if (typeof x === ‘string’ && typeof y === ‘string’) {
return x + y; // 字符串拼接
} else if (typeof x === ‘number’ && typeof y === ‘number’) {
return x + y; // 数字相加
}
// 可以添加错误处理或其他逻辑
throw new Error(“Invalid arguments”);
}
// 使用重载
let sResult = add(“hello”, “world”); // 根据第一个签名调用,sResult 是 string 类型
let nResult = add(10, 20); // 根据第二个签名调用,nResult 是 number 类型
let aResult = add([1, 2], [3, 4]); // 根据第三个签名调用,aResult 是 number[] 类型
// 错误:没有匹配的重载签名
// add(true, false);
“`
通过函数类型和重载,TypeScript 使得函数的使用更加类型安全和清晰。
第五章:接口 (Interfaces)
接口(Interface)是 TypeScript 中非常核心的概念,它用于定义对象的形状(Shape)。接口可以用来约束变量、函数参数、函数返回值、类等等。
定义接口
使用 interface
关键字定义接口,然后在其中列出属性及其类型。
“`typescript
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
console.log(Hello, ${person.name}! You are ${person.age} years old.
);
}
let user = { name: “Alice”, age: 30 };
greet(user); // 正确,user 对象符合 Person 接口的形状
// 错误:对象缺少属性 age
// greet({ name: “Bob” });
// 错误:对象属性 age 类型不正确
// greet({ name: “Charlie”, age: “unknown” });
// 可以有额外属性,但如果直接将对象字面量赋给接口类型变量,会有严格检查(”Excess Property Checks”)
let userWithJob = { name: “David”, age: 40, job: “Engineer” };
greet(userWithJob); // 正确,将符合接口的对象赋给接口类型变量是允许额外属性的
// 错误:对象字面量直接赋给接口类型变量时,不允许有接口中未定义的属性
// let userWithJobStrict: Person = { name: “David”, age: 40, job: “Engineer” };
“`
对象字面量直接赋值时会进行严格检查,防止潜在的拼写错误。如果确实需要额外属性,可以使用类型断言或索引签名。
可选属性 (Optional Properties)
接口中的属性可以使用 ?
标记为可选。
“`typescript
interface Config {
path: string;
debug?: boolean; // 可选属性
}
function initialize(config: Config) {
// …
if (config.debug) {
console.log(“Debug mode is on.”);
}
}
initialize({ path: “/data” }); // 正确,debug 属性是可选的
initialize({ path: “/data”, debug: true }); // 正确
“`
只读属性 (Readonly Properties)
接口中的属性可以使用 readonly
标记为只读。这意味着该属性在对象创建后不能被修改。
“`typescript
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // 错误:x 是只读属性
// ReadonlyArray
let a: number[] = [1, 2, 3, 4];
let roa: ReadonlyArray
// roa[0] = 12; // 错误:只读数组不允许修改元素
// roa.push(5); // 错误:只读数组不允许添加元素
// roa.length = 100; // 错误:只读数组不允许修改 length
// a = roa; // 错误:只读数组不能直接赋值给普通数组 (需要类型断言或拷贝)
a = roa as number[]; // 可以通过类型断言
“`
函数类型接口
接口也可以描述函数的类型签名。
“`typescript
interface SearchFunc {
(source: string, subString: string): boolean; // 接受两个 string 参数,返回 boolean
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean { // 参数名可以不同,但类型和数量必须匹配
let result = src.search(sub);
return result > -1;
}
// 错误:参数类型不匹配
// mySearch = function(src: number, sub: number): boolean { return false; }
// 错误:返回值类型不匹配
// mySearch = function(src: string, sub: string): number { return 0; }
“`
可索引的类型 (Indexable Types)
接口可以描述具有索引签名的类型,比如数组或字典(对象)。
“`typescript
interface StringArray {
[index: number]: string; // 索引是 number 类型,值是 string 类型
}
let myArray: StringArray;
myArray = [“Bob”, “Fred”];
console.log(myArray[0]); // 输出 “Bob”
// myArray[2] = 100; // 错误:索引为 number 时,值必须是 string
interface StringDictionary {
[propName: string]: string; // 索引是 string 类型,值是 string 类型
}
let myDictionary: StringDictionary;
myDictionary = { greeting: “hello”, farewell: “goodbye” };
console.log(myDictionary[“greeting”]); // 输出 “hello”
// myDictionary[“age”] = 30; // 错误:索引为 string 时,值必须是 string
``
obj[10]
一个接口可以同时拥有数字索引签名和字符串索引签名,但数字索引的返回值必须是字符串索引返回值的子类型(因为最终会转换为
obj[“10”]`)。
“`typescript
interface MixedDictionary {
[index: number]: number; // 数字索引,值是 number
[propName: string]: number | string; // 字符串索引,值是 number 或 string
// 这里要求数字索引的值 (number) 必须是字符串索引的值 (number | string) 的子类型,满足。
// 如果字符串索引的值是 string,则会报错。
}
let mix: MixedDictionary = {
0: 10,
“prop1”: 20,
“prop2”: “hello”
}; // 正确
// interface ErrorDictionary {
// [index: number]: number;
// [propName: string]: string; // 错误:number 不是 string 的子类型
// }
“`
接口继承 (Extending Interfaces)
接口可以继承一个或多个现有接口,从而获得被继承接口的所有成员。
“`typescript
interface Shape {
color: string;
}
interface Square extends Shape { // Square 继承 Shape
sideLength: number;
}
let square: Square = { color: “blue”, sideLength: 10 }; // 必须包含 color 和 sideLength
interface PenStroke {
penWidth: number;
}
interface SquareWithPen extends Square, PenStroke { // 可以继承多个接口
// 拥有 color, sideLength, penWidth
}
let squareWithPen: SquareWithPen = { color: “red”, sideLength: 20, penWidth: 5 };
“`
接口是定义对象结构和契约的强大工具,在 TypeScript 中被广泛使用。
第六章:类型别名 (Type Aliases) 与 联合/交叉类型 (Union/Intersection Types)
除了接口,TypeScript 还提供了类型别名。同时,联合类型和交叉类型是组合现有类型的重要方式。
类型别名 (Type Aliases)
类型别名使用 type
关键字,可以给任何类型起一个新名字,包括原始类型、联合类型、交叉类型、对象类型、元组等。
“`typescript
// 给原始类型起别名
type MyString = string;
let s: MyString = “hello”;
// 给对象类型起别名
type Point = {
x: number;
y: number;
};
let point: Point = { x: 10, y: 20 };
// 给联合类型起别名
type StringOrNumber = string | number;
let value: StringOrNumber = “abc”;
value = 123;
// 给函数类型起别名
type GreetFunc = (name: string) => string;
let myGreet: GreetFunc = (name) => Hello, ${name}
;
// 给元组起别名
type MyTuple = [number, string];
let tuple: MyTuple = [1, “hello”];
“`
interface
vs type
别名
在很多情况下,接口和类型别名可以互换使用来描述对象结构。然而,它们之间有一些重要的区别:
- 扩展性 (Extendability):
interface
可以被其他interface
和class
实现/继承。type
别名不能被“实现”或“继承”,但可以使用交叉类型 (&) 来“组合”类型。interface
可以声明合并 (Declaration Merging),即如果你在代码中多次声明同一个名字的接口,它们会自动合并为一个接口。type
别名不支持声明合并。
- 可以描述的类型范围:
type
别名可以用于给原始类型、联合类型、交叉类型、元组、函数等任何类型起别名。interface
主要用于描述对象的形状(包括函数类型、可索引类型)。
- 使用建议:
- 如果是在定义公共 API 的形状(例如库的类型定义),或者希望利用声明合并特性,通常推荐使用
interface
。 - 如果需要给联合类型、交叉类型、原始类型、元组等起别名,或者需要使用一些高级类型操作(如映射类型,将在后续高级教程中介绍),则必须使用
type
。 - 对于简单的对象类型,选择
interface
或type
更多是风格偏好。由于interface
具有更好的扩展性,一些团队倾向于优先使用interface
来定义对象结构。
- 如果是在定义公共 API 的形状(例如库的类型定义),或者希望利用声明合并特性,通常推荐使用
联合类型 (Union Types)
联合类型表示一个变量可以拥有多种类型之一。使用 |
符号分隔。
“`typescript
function formatId(id: number | string) {
if (typeof id === ‘string’) {
console.log(id.toUpperCase()); // 在这个块内,id 被缩小为 string 类型
} else {
console.log(id.toFixed(2)); // 在这个块内,id 被缩小为 number 类型
}
}
formatId(123);
formatId(“abc”);
// formatId(true); // 错误:参数类型必须是 number 或 string
``
typeof
联合类型常与类型守卫 (,
instanceof` 等) 结合使用,以便在特定的代码块中对变量进行类型缩小 (Type Narrowing)。
交叉类型 (Intersection Types)
交叉类型将多个类型合并为一个类型。它包含了所有类型的成员。使用 &
符号分隔。
“`typescript
interface Drawable {
draw(): void;
}
interface Resizable {
resize(): void;
}
type UIElement = Drawable & Resizable; // UIElement 必须同时具备 draw() 和 resize() 方法
let button: UIElement = {
draw() { console.log(“Drawing button”); },
resize() { console.log(“Resizing button”); }
};
button.draw();
button.resize();
// 错误:缺少 resize 方法
// let incompleteButton: UIElement = {
// draw() { console.log(“Drawing incomplete button”); }
// };
“`
交叉类型常用于组合多个接口或对象类型,创建一个新的、包含所有属性的类型。
第七章:类 (Classes)
TypeScript 对 ES6 的类提供了全面的支持,并在此基础上增加了类型注解、访问修饰符等特性。
类的定义
使用 class
关键字定义类,包含属性和方法。
“`typescript
class Greeter {
greeting: string; // 属性
constructor(message: string) { // 构造函数
this.greeting = message;
}
greet(): string { // 方法
return “Hello, ” + this.greeting;
}
}
let greeter = new Greeter(“world”);
console.log(greeter.greet()); // 输出 “Hello, world”
“`
修饰符 (Modifiers)
TypeScript 提供了三个访问修饰符来控制类成员的可访问性:public
, private
, protected
。
public
(默认): 类内部、子类、外部都可以访问。private
: 只有在声明它的类内部可以访问。protected
: 在类内部和其子类中可以访问,但在类的外部不能访问。
“`typescript
class Animal {
public name: string; // public 属性,默认就是 public
private age: number; // private 属性,只能在 Animal 类内部访问
protected species: string; // protected 属性,在 Animal 类及其子类内部访问
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this.species = species;
}
public move(distanceInMeters: number = 0) {
console.log(${this.name} moved ${distanceInMeters}m.
);
console.log(Age: ${this.age}
); // 可以访问 private age
}
}
class Dog extends Animal {
constructor(name: string, age: number) {
super(name, age, “Canine”);
}
bark() {
console.log(“Woof Woof!”);
console.log(Species: ${this.species}
); // 可以访问 protected species
// console.log(Age: ${this.age}
); // 错误:不能访问 private age
}
}
let animal = new Animal(“Generic Animal”, 5, “Unknown”);
console.log(animal.name); // 正确,public 属性可访问
animal.move(10);
// console.log(animal.age); // 错误:private 属性不可访问
// console.log(animal.species); // 错误:protected 属性在外部不可访问
let dog = new Dog(“Buddy”, 3);
console.log(dog.name); // 正确
dog.bark();
dog.move(5);
// console.log(dog.species); // 错误:protected 属性在外部不可访问
“`
构造函数参数属性 (Parameter Properties)
可以在构造函数参数前面添加修饰符 (public
, private
, protected
, readonly
),这会同时声明并初始化对应名称的类成员。
“`typescript
class AnimalShorthand {
// public name 和 readonly species 会被自动创建并初始化
constructor(public name: string, private age: number, readonly species: string) {
// 无需再写 this.name = name; this.age = age; this.species = species;
}
introduce() {
console.log(My name is ${this.name}, I am ${this.age} years old, a ${this.species}.
);
}
}
let cat = new AnimalShorthand(“Whiskers”, 4, “Feline”);
cat.introduce(); // 输出 My name is Whiskers, I am 4 years old, a Feline.
console.log(cat.name); // Whiskers
// console.log(cat.age); // 错误:private
// cat.species = “Canis”; // 错误:readonly
“`
继承 (Inheritance)
使用 extends
关键字实现类继承。子类可以使用 super()
调用父类的构造函数和方法。
``typescript
${this.name} moved ${distanceInMeters}m.`);
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); // 调用父类 Animal 的构造函数
}
move(distanceInMeters = 5) { // 重写父类方法
console.log(“Slithering…”);
super.move(distanceInMeters); // 调用父类的 move 方法
}
}
class Horse extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) { // 重写父类方法
console.log(“Galloping…”);
super.move(distanceInMeters);
}
}
let sam = new Snake(“Sammy the Python”);
let tom: Animal = new Horse(“Tommy the Palomino”); // 多态:子类对象可以赋值给父类类型变量
sam.move(); // 输出 Slithering… Sammy the Python moved 5m.
tom.move(34); // 输出 Galloping… Tommy the Palomino moved 34m.
“`
实现接口 (Implementing Interfaces)
类可以使用 implements
关键字来实现一个或多个接口。类必须实现接口中定义的所有非可选成员。
“`typescript
interface ClockInterface {
currentTime: Date; // 属性
setTime(d: Date): void; // 方法
}
class Clock implements ClockInterface {
currentTime: Date = new Date(); // 必须实现 currentTime 属性
setTime(d: Date) { // 必须实现 setTime 方法
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
// 接口也可以描述构造函数签名,但这需要一个特殊的写法
// interface ClockConstructor {
// new (hour: number, minute: number): ClockInterface; // 描述构造函数
// }
// function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
// return new ctor(hour, minute);
// }
// class DigitalClock implements ClockInterface {
// currentTime: Date;
// constructor(h: number, m: number) { / … / }
// setTime(d: Date) { this.currentTime = d; }
// }
// let digital = createClock(DigitalClock, 12, 30);
“`
抽象类 (Abstract Classes)
抽象类不能直接实例化,只能作为其他类的基类。抽象方法在抽象类中定义,不包含具体实现,必须由派生类实现。
“`typescript
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log(Department name: ${this.name}
);
}
abstract printMeeting(): void; // 抽象方法,必须在派生类中实现
}
class AccountingDepartment extends Department {
constructor() {
super(‘Accounting and Auditing’); // 调用父类构造函数
}
printMeeting(): void { // 实现抽象方法
console.log(‘The Accounting Department meets each Monday at 10am.’);
}
generateReports(): void {
console.log(‘Generating accounting reports…’);
}
}
// let department = new Department(); // 错误:不能创建抽象类的实例
let accounting = new AccountingDepartment(); // 正确
accounting.printName(); // 输出 Department name: Accounting and Auditing
accounting.printMeeting(); // 输出 The Accounting Department meets each Monday at 10am.
accounting.generateReports(); // 输出 Generating accounting reports…
“`
静态成员 (Static Members)
类成员可以标记为 static
。静态成员不属于类的某个具体实例,而是属于类本身。可以直接通过类名访问。
“`typescript
class Grid {
static origin = { x: 0, y: 0 }; // 静态属性
calculateDistanceFromOrigin(point: { x: number; y: number; }) {
let xDist = (point.x – Grid.origin.x); // 通过类名访问静态属性
let yDist = (point.y – Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(public scale: number) { } // 实例属性 scale
}
let grid1 = new Grid(1.0);
let grid2 = new Grid(5.0);
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));
console.log(Grid.origin); // 直接通过类名访问静态属性
// console.log(grid1.origin); // 错误:静态属性不能通过实例访问
“`
通过类,TypeScript 提供了强大的面向对象编程能力,并结合类型系统确保了代码的结构化和安全性。
第八章:模块 (Modules)
在现代 JavaScript 开发中,模块是组织代码的基本方式。TypeScript 完全支持 ES Modules (import
/export
)。使用模块可以将代码分割到不同的文件中,避免命名冲突,提高代码的可维护性和复用性。
导出 (Exporting)
在文件中使用 export
关键字可以导出变量、函数、类、接口、类型别名等,使其可以在其他文件中被引用。
“`typescript
// utils.ts
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export interface Config {
url: string;
timeout: number;
}
export class Calculator {
// …
}
// 或者批量导出
// const PI = 3.14159;
// function add(a: number, b: number): number { return a + b; }
// export { PI, add };
// 默认导出 (Default Export): 每个模块只能有一个默认导出
// export default class MyService {
// // …
// }
// 或者
// class MyService { / … / }
// export default MyService;
“`
导入 (Importing)
在另一个文件中使用 import
关键字可以导入其他模块导出的成员。
“`typescript
// main.ts
import { PI, add, Config, Calculator } from “./utils”; // 导入特定成员
console.log(PI);
console.log(add(2, 3));
let config: Config = { url: “http://example.com”, timeout: 5000 };
let calc = new Calculator();
// 导入默认导出
// import MyService from “./utils”; // 导入默认导出,名字可以自定义
// let service = new MyService();
// 导入模块中的所有成员并创建一个命名空间对象
// import * as Utils from “./utils”;
// console.log(Utils.PI);
// console.log(Utils.add(5, 6));
“`
模块化是大型项目开发的基石。TypeScript 的模块系统与 ES Modules 标准保持一致,使得在不同的 JavaScript 环境中迁移或集成变得更加容易。
模块编译选项
tsconfig.json
中的 module
选项决定了 TypeScript 代码被编译成哪种模块系统(如 commonjs
, amd
, umd
, es2015
, esnext
等)。选择哪个取决于你的目标运行环境和使用的打包工具(如 Webpack)。
第九章:高级主题简述
TypeScript 还有一些更高级的特性,它们在特定场景下非常有用。这里我们简要介绍几个重要的概念,为进一步学习打下基础。
类型断言 (Type Assertions)
类型断言用来告诉编译器你比它更清楚一个变量的实际类型。这并不是类型转换,也不会影响运行时代码。它仅仅是在编译阶段指导类型检查。
有两种形式:
- “尖括号”语法:
<Type>value
as
语法:value as Type
推荐使用 as
语法,因为它在 JSX 中与尖括号语法不会产生冲突。
“`typescript
let someValue: any = “this is a string”;
// 使用 as 语法
let strLength: number = (someValue as string).length;
console.log(strLength); // 输出 16
// 使用尖括号语法 (在 JSX 中可能与标签语法冲突)
let strLength2: number = (
console.log(strLength2); // 输出 16
// 注意:如果你断言的类型与实际类型差距太大,可能会导致运行时错误,因为类型断言不进行运行时检查。
// let num: number = (someValue as number); // 编译时不报错,但运行时 (num.toFixed) 会出错
// console.log(num.toFixed(2)); // 运行时错误! someValue 实际上是 string
“`
类型断言应该谨慎使用,只有当你明确知道某个变量的真实类型,并且 TypeScript 无法自动推断时才使用。过度使用类型断言可能会绕过类型检查,引入潜在的运行时错误。
类型守卫 (Type Guards)
类型守卫是一种检查,它能在特定的作用域内缩小变量的类型。TypeScript 提供了多种内置的类型守卫,你也可以创建自定义的类型守卫。
-
typeof
守卫: 检查变量的原始类型。typescript
function printId(id: number | string) {
if (typeof id === 'string') {
console.log(id.toUpperCase()); // id 在此块内是 string
} else {
console.log(id.toFixed(2)); // id 在此块内是 number
}
} -
instanceof
守卫: 检查变量是否是某个类的实例。“`typescript
class Animal {
name: string;
constructor(name: string) { this.name = name; }
}
class Fish extends Animal { swim() { console.log(“Swim”); } }
class Bird extends Animal { fly() { console.log(“Fly”); } }function move(animal: Fish | Bird) {
if (animal instanceof Fish) {
animal.swim(); // animal 在此块内是 Fish
} else {
animal.fly(); // animal 在此块内是 Bird
}
}
“` -
自定义类型守卫: 创建一个返回值为
parameterName is Type
形式的函数。“`typescript
interface Car {
drive(): void;
}
interface Bike {
pedal(): void;
}// 自定义类型守卫函数
function isCar(vehicle: Car | Bike): vehicle is Car {
// 检查 vehicle 是否包含 drive 方法
return (vehicle as Car).drive !== undefined;
}function start(vehicle: Car | Bike) {
if (isCar(vehicle)) {
vehicle.drive(); // vehicle 在此块内是 Car
} else {
vehicle.pedal(); // vehicle 在此块内是 Bike
}
}
“`
类型守卫是处理联合类型时非常重要的技术。
第十章:泛型 (Generics)
泛型(Generics)是 TypeScript 中实现代码可重用性的强大工具。它允许你在定义函数、接口或类时使用类型参数,从而使其能够处理多种不同类型的数据,而不是局限于单一类型。
为什么需要泛型?
考虑一个函数,它返回你传递给它的参数:
“`typescript
function identity(arg: number): number {
return arg;
}
function identity(arg: string): string {
return arg;
}
// … 如果要支持其他类型,需要写很多重载
``
any`:
或者使用
“`typescript
function identity(arg: any): any {
return arg;
}
let result = identity(“myString”); // result 是 any 类型,失去了类型信息
// result.toFixed(2); // 编译时不报错,运行时可能出错
``
any
使用虽然通用,但丢失了参数类型和返回值类型之间的关联。我们希望的是,如果传入的是
number,返回的就是
number;如果传入的是
string,返回的就是
string`。这就是泛型的用武之地。
泛型的基本使用
使用尖括号 <>
包裹类型参数(通常用 T
表示 Type)。
“`typescript
// 使用泛型 T
function identity
return arg;
}
// 调用时可以显式指定类型参数 (通常不需要,编译器会推断)
let output1 = identity
// 或者让编译器自动推断
let output2 = identity(“myString”); // output2 是 string 类型
let output3 = identity(123); // output3 是 number 类型
let output4 = identity(true); // output4 是 boolean 类型
``
T
通过泛型,我们捕获了参数
arg` 的类型,并用它作为返回类型。这样,函数变得通用,同时保留了类型信息。
泛型数组
“`typescript
function loggingIdentity
console.log(arg.length); // 可以在 T[] 上使用数组属性 length
return arg;
}
// 或者使用 Array
// function loggingIdentity
// console.log(arg.length);
// return arg;
// }
let numbers = loggingIdentity([1, 2, 3]); // numbers 是 number[]
let strings = loggingIdentity([“a”, “b”]); // strings 是 string[]
“`
泛型接口
接口本身也可以是泛型的。
“`typescript
interface GenericIdentityFn
(arg: T): T;
}
function identity
return arg;
}
let myIdentity: GenericIdentityFn
let myIdentity2: GenericIdentityFn
console.log(myIdentity(100)); // 参数和返回值都是 number
console.log(myIdentity2(“test”)); // 参数和返回值都是 string
“`
泛型类
类也可以是泛型的。但类的静态成员不能使用类的泛型参数。
“`typescript
class GenericNumber
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, addFunc: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = addFunc;
}
}
let myGenericNumber = new GenericNumber
console.log(myGenericNumber.add(5, 10)); // 输出 15
let myGenericString = new GenericNumber
console.log(myGenericString.add(“Hello, “, “TypeScript!”)); // 输出 Hello, TypeScript!
“`
泛型约束 (Generic Constraints)
有时你希望泛型参数具备某些属性(比如 length
属性)。这时可以使用泛型约束。
“`typescript
interface Lengthwise {
length: number;
}
// 约束 T 必须实现 Lengthwise 接口
function loggingIdentity
console.log(arg.length); // 现在可以在 arg 上访问 length 属性了
return arg;
}
loggingIdentity(“hello”); // 正确,string 有 length 属性
loggingIdentity([1, 2, 3]); // 正确,数组有 length 属性
// loggingIdentity(3); // 错误:number 没有 length 属性
“`
泛型约束使得泛型更加灵活和安全。
其他常用泛型命名
除了 T
,常见的泛型参数名还包括:
* K
(Key): 表示对象属性的键类型。
* V
(Value): 表示对象属性的值类型。
* E
(Element): 表示集合中的元素类型。
例如:
“`typescript
function getProperty
return obj[key]; // K extends keyof T 表示 K 必须是 T 的属性名组成的联合类型
}
let obj = { a: 1, b: 2, c: 3 };
console.log(getProperty(obj, “a”)); // 正确
// console.log(getProperty(obj, “d”)); // 错误:d 不是 obj 的属性名
“`
泛型是 TypeScript 中构建可重用组件、提高代码灵活性的重要特性。掌握泛型是迈向 TypeScript 高级开发的关键一步。
第十一章:配置 TypeScript (Configuring TypeScript)
tsconfig.json
文件是 TypeScript 项目的核心配置文件,它告诉编译器如何编译项目中的 .ts
文件。通过配置这个文件,你可以控制输出的 JavaScript 版本、模块系统、严格性检查级别、输出目录等等。
生成 tsconfig.json
如前所述,在项目根目录运行 tsc --init
命令可以生成一个带有注释的默认 tsconfig.json
文件。
关键配置选项 (compilerOptions)
compilerOptions
部分包含了大部分编译器配置。以下是一些常用且重要的选项:
target
: 指定编译目标 JavaScript 版本。可选值包括 “es3”, “es5”, “es2015”, “es2016”, “es2017”, “es2018”, “es2019”, “es2020”, “es2021”, “es2022”, “esnext”。选择的版本会影响最终生成的 JavaScript 代码的语法特性。例如,如果目标是 “es5″,class 语法会被编译成基于原型的代码。module
: 指定生成的模块系统。可选值包括 “none”, “commonjs”, “amd”, “system”, “umd”, “es2015”, “es2020”, “ESNext”, “node16”, “nodenext”。根据你的项目环境(Node.js、浏览器、打包工具)选择合适的模块系统。strict
: 开启所有严格类型检查选项。强烈建议启用此选项! 开启后,会同时启用noImplicitAny
,strictNullChecks
,strictFunctionTypes
,strictBindCallApply
,strictPropertyInitialization
,noImplicitThis
,useUnknownInCatchVariables
等。这将极大地提升代码的健壮性。strictNullChecks
: (包含在strict
中) 启用严格的 null 和 undefined 检查,防止空引用错误。esModuleInterop
: 允许使用 ES Modules 风格的导入方式(import React from 'react'
)来导入 CommonJS 模块(const React = require('react')
),提供了更好的互操作性。通常建议启用。outDir
: 指定编译输出的 JavaScript 文件存放的目录。rootDir
: 指定 TypeScript 源文件的根目录。编译器会根据rootDir
和outDir
的相对路径结构来生成输出文件。sourceMap
: 生成.map
文件,方便在浏览器或 Node.js 调试时映射回原始的 TypeScript 代码。include
: 一个文件路径数组,指定需要编译的文件或文件夹。支持 glob 模式。exclude
: 一个文件路径数组,指定在编译过程中需要排除的文件或文件夹。通常用于排除node_modules
或构建输出目录。files
: 一个文件路径数组,精确指定需要编译的入口文件列表。如果使用了include
或exclude
,通常不需要指定files
。
示例 tsconfig.json
json
{
"compilerOptions": {
"target": "es2020", // 编译到 ES2020
"module": "esnext", // 使用 ES Modules
"strict": true, // 开启所有严格检查
"esModuleInterop": true, // 开启 ES Module 互操作性
"skipLibCheck": true, // 跳过声明文件(.d.ts)的类型检查,提高编译速度(但可能隐藏第三方库的问题)
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"outDir": "./dist", // 输出到 dist 目录
"rootDir": "./src", // 源文件在 src 目录
"sourceMap": true // 生成 source map
},
"include": [
"./src/**/*.ts" // 包含 src 目录下所有 .ts 文件及其子目录
],
"exclude": [
"node_modules", // 排除 node_modules 目录
"./dist" // 排除输出目录
]
}
通过合理配置 tsconfig.json
,你可以根据项目的需求定制编译行为,并启用严格的类型检查,从而提高代码质量。
第十二章:将 TypeScript 集成到项目中
在实际开发中,TypeScript 通常不是独立使用的,而是与各种构建工具、框架和库集成。
-
与构建工具集成:
- Webpack: 使用
ts-loader
或awesome-typescript-loader
将.ts
文件作为模块加载。 - Parcel: 对 TypeScript 有内置支持,配置简单。
- Rollup: 使用
@rollup/plugin-typescript
或@rollup/plugin-babel
(配置@babel/preset-typescript
)。 - Vite: 内置支持 TypeScript,速度非常快。
- Webpack: 使用
-
与前端框架集成:
- React: 创建 React 项目时可以使用官方提供的 TypeScript 模板 (
create-react-app my-app --template typescript
)。也可以通过配置 Babel (@babel/preset-typescript
) 或 Webpack loaders 集成。使用类型定义来约束组件 props、state、事件等。 - Vue: Vue CLI 创建项目时可以选择 TypeScript 选项。Vue 3 对 TypeScript 支持非常好, Composition API 和 SFC (
.vue
文件) 都有很好的类型提示。 - Angular: Angular 完全使用 TypeScript 构建,对 TypeScript 有天然的支持。
- React: 创建 React 项目时可以使用官方提供的 TypeScript 模板 (
-
类型声明文件 (
.d.ts
):- JavaScript 库或框架本身可能是用 JavaScript 编写的,但为了让 TypeScript 项目也能安全地使用它们,需要提供类型声明文件(
.d.ts
)。 - 许多流行的库(如 React, Lodash, Express 等)都在
@types
组织下提供了官方或社区维护的类型声明包。例如,安装@types/react
就可以在 TypeScript 项目中获得 React 的类型支持。
bash
npm install --save-dev @types/react @types/react-dom - 如果你使用的库没有提供类型声明,你可以尝试查找社区提供的,或者自己编写。
- JavaScript 库或框架本身可能是用 JavaScript 编写的,但为了让 TypeScript 项目也能安全地使用它们,需要提供类型声明文件(
-
Linter 和 Formatter:
- 使用 ESLint 结合
@typescript-eslint/eslint-plugin
和@typescript-eslint/parser
可以对 TypeScript 代码进行静态分析和强制代码风格。 - Prettier 是一个流行的代码格式化工具,它也支持 TypeScript。
- 使用 ESLint 结合
集成过程的核心是配置构建工具,使其能够识别和处理 .ts
文件,并利用 @types
包获取第三方库的类型定义。
结论
恭喜你!通过本文的学习,你已经了解了 TypeScript 的基础知识、核心概念以及如何在项目中使用它。我们从环境搭建开始,深入学习了基础类型、复杂类型结构、函数、接口、类型别名、联合/交叉类型、类、模块,并简要介绍了泛型、类型断言、类型守卫以及如何配置和集成 TypeScript。
TypeScript 为 JavaScript 开发带来了静态类型的强大优势,能够帮助你在开发早期捕获错误、提高代码可读性、增强可维护性,并极大地提升开发体验。虽然引入类型系统需要额外的学习成本,但从长远来看,尤其是在大型复杂项目或团队协作中,它的收益是巨大的。
下一步学习建议:
- 动手实践: 最好的学习方式是实践。尝试将已有的 JavaScript 项目迁移到 TypeScript,或开始一个新的 TypeScript 项目。
- 深入高级类型: 探索 TypeScript 的高级类型系统,如映射类型 (Mapped Types)、条件类型 (Conditional Types) 等,它们能让你处理更复杂的类型逻辑。
- 阅读官方文档: TypeScript 官方文档 (https://www.typescriptlang.org/docs/) 是最权威的学习资料,包含了更多详细信息和高级主题。
- 学习设计模式: 了解如何在 TypeScript 中应用常见的设计模式。
- 阅读优秀的开源项目代码: 阅读使用 TypeScript 编写的知名开源项目,学习它们如何组织代码和使用类型。
- 参与社区: 加入 TypeScript 社区,提问、分享经验、甚至尝试为类型声明文件做贡献。
从零开始只是第一步,TypeScript 的世界还有很多精彩等待你去探索。持续学习、不断实践,你将在构建更健壮、更可维护的应用程序的道路上越走越远!
祝你学习顺利,享受 TypeScript 带来的便利和乐趣!