TypeScript 教程:从零开始学 – wiki基地


TypeScript 教程:从零开始学

迈向更健壮、更可维护的 JavaScript 世界

引言

前端开发的世界日新月异,JavaScript 作为其基石,以其灵活性和易上手性赢得了全球开发者的青睐。然而,随着项目规模的扩大和复杂度的增加,JavaScript 的动态类型特性也带来了挑战:潜在的运行时错误难以在开发阶段被发现,代码的可读性和可维护性下降,大型团队协作时更容易出错。

正是在这样的背景下,TypeScript 应运而生。

TypeScript 是什么?

简单来说,TypeScript 是 JavaScript 的一个超集。这意味着任何合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 在 JavaScript 的基础上增加了静态类型系统以及其他面向对象特性(如接口、抽象类等)。TypeScript 代码需要通过编译器转换(或称“编译”)成纯粹的 JavaScript 代码,才能在浏览器或 Node.js 环境中运行。

为什么学习 TypeScript?

学习 TypeScript 的好处不胜枚举,尤其对于希望提升开发效率、构建大型应用或进行团队协作的开发者而言:

  1. 提前发现错误 (Early Error Detection): 类型检查在编译阶段进行,而不是等到运行时。这能捕获大量的潜在错误,比如拼写错误的函数名、使用了错误类型的数据等,大大减少调试时间。
  2. 增强代码可读性与可维护性: 类型注解清晰地表明了变量、函数参数、返回值的预期类型,使得代码意图更加明确,更易于理解和修改。
  3. 提升开发效率与体验:
    • 强大的编辑器支持: VS Code、WebStorm 等现代编辑器对 TypeScript 有着出色的支持,提供智能代码补全、类型提示、重构、跳转到定义等功能,极大地提升了开发效率。
    • 重构更安全: 在大型项目中进行代码重构时,类型系统可以帮助你确保改动不会破坏其他部分的逻辑。
  4. 更好的团队协作: 明确的类型定义构成了契约,使得团队成员之间更容易理解和集成彼此的代码。
  5. 拥抱最新的 ECMAScript 特性: TypeScript 通常会支持最新的 ECMAScript 标准特性,即使你的目标运行环境(如旧版浏览器)不支持,TypeScript 编译器也能将其转换成兼容的代码。

如果你已经具备 JavaScript 基础,并且希望提升自己的开发技能,构建更稳定、更易于维护的应用,那么学习 TypeScript 是一个非常值得的投资。

本文将带你从零开始,一步步深入了解 TypeScript 的核心概念和用法。让我们开始这段学习之旅吧!

第一章:准备工作与第一个 TypeScript 文件

开始学习之前,我们需要搭建开发环境。

  1. 安装 Node.js: TypeScript 编译器 tsc 是一个 Node.js 包。请确保你的系统中已经安装了 Node.js(建议选择 LTS 版本)。可以访问 Node.js 官网 (https://nodejs.org/) 下载安装包。安装完成后,打开终端或命令行工具,输入 node -vnpm -v(或者 yarn -v 如果你使用 Yarn 包管理器),检查是否安装成功。
  2. 安装 TypeScript: 打开终端或命令行工具,使用 npm 或 Yarn 全局安装 TypeScript:
    bash
    npm install -g typescript
    # 或者使用 yarn
    # yarn global add typescript

    安装完成后,输入 tsc -v 检查 TypeScript 编译器是否安装成功并显示版本号。

编写第一个 TypeScript 文件

现在,我们来创建一个简单的 TypeScript 文件。

  1. 在你喜欢的位置创建一个新文件夹,比如 my-ts-project
  2. 在该文件夹内创建一个名为 hello.ts 的文件(注意后缀是 .ts)。
  3. 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: stringlet num: number 中的 : string: number,这就是 TypeScript 的类型注解。

编译 TypeScript 代码

TypeScript 代码不能直接在浏览器或 Node.js 中运行,需要先编译成 JavaScript。

  1. 打开终端或命令行工具,进入到 my-ts-project 文件夹。
  2. 运行 TypeScript 编译器命令:

    bash
    tsc hello.ts

  3. 如果代码没有类型错误,该命令会在同一目录下生成一个 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 代码中被移除了。

  4. 处理类型错误: 如果你取消注释 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.
    ``
    这就是 TypeScript 在编译阶段捕获的类型错误!它明确告诉你不能将一个
    string类型的值赋给一个number` 类型的变量。

运行编译后的 JavaScript 文件

现在,我们可以运行生成的 hello.js 文件:

bash
node hello.js

你将在终端看到输出:Hello, TypeScript User

使用 tsconfig.json 配置项目

对于稍微复杂的项目,手动编译每个 .ts 文件会非常繁琐。更常见的方式是使用 tsconfig.json 文件来配置 TypeScript 项目。

  1. my-ts-project 文件夹的根目录下,运行以下命令生成一个默认的 tsconfig.json 文件:

    bash
    tsc --init

  2. 该命令会生成一个包含许多配置选项的 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 目录下
    ]
    }

  3. 有了 tsconfig.json 文件后,只需在项目根目录运行 tsc 命令(不带文件名),编译器就会查找并使用 tsconfig.json 中的配置来编译整个项目。

    bash
    tsc

    如果配置了 outDir: "./dist",编译后的 hello.js 将会被输出到 dist 文件夹中。

  4. 监听模式: 在开发过程中,你可能希望在修改 .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 的所有原始类型:

  1. number: 表示数字,包括整数和浮点数。

    typescript
    let decimal: number = 6;
    let hex: number = 0xf00d;
    let binary: number = 0b1010;
    let octal: number = 0o744;

  2. string: 表示文本数据。

    typescript
    let color: string = "blue";
    color = 'red'; // 单引号双引号都可以
    let fullName: string = `Bob Adams`; // 模板字符串
    let greeting: string = `Hello, my name is ${fullName}.`;

  3. boolean: 表示布尔值,truefalse

    typescript
    let isApproved: boolean = true;

  4. nullundefined: 分别表示 nullundefined 值。默认情况下,nullundefined 是所有类型的子类型,可以赋值给任何类型。然而,在启用 strictNullChecks 选项时,它们只能赋值给 nullundefined 类型自身,或者 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` 选项,以提高代码健壮性。

  5. symbol: 表示全局唯一引用。

    typescript
    const sym1 = Symbol('key');
    const sym2 = Symbol('key');
    // sym1 === sym2 // false

  6. bigint: 表示任意精度的整数。需要目标环境支持(如 ES2020 及以上)。

    typescript
    let bigIntVar: bigint = 100n;
    // let numberVar: number = bigIntVar; // 错误:不能将 bigint 赋给 number

特殊类型

  1. 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
    “`

  2. unknown: 表示未知类型。与 any 类似,unknown 类型的变量可以被赋予任何类型的值。但是,与 any 不同的是,你不能直接对 unknown 类型的变量进行任意操作(比如调用方法或访问属性),除非你先进行类型检查或类型断言,证明其是某个具体的类型。这使得 unknownany 更安全。

    “`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
    }
    “`

  3. 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
    “`

  4. never: 表示永远不会发生的类型。例如,一个总是抛出异常的函数,或者一个无限循环的函数的返回值类型就是 nevernever 是任何类型的子类型,但没有任何类型是 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)

表示同类型元素组成的列表。有两种定义方式:

  1. 元素类型后面接 []:

    typescript
    let list: number[] = [1, 2, 3];
    // list.push("4"); // 错误:不能添加 string

  2. 使用泛型 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: 虽然对象字面量可以直接定义类型,但对于复杂的或需要复用的对象结构,通常使用 interfacetype 别名(将在后续章节介绍)来定义,这样更清晰且易于管理。

至此,我们了解了如何使用基础类型和一些基本的结构类型来描述数据。接下来,我们将看看如何在函数中使用类型。

第四章:函数 (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 = a;
// 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 可以被其他 interfaceclass 实现/继承。
    • type 别名不能被“实现”或“继承”,但可以使用交叉类型 (&) 来“组合”类型。
    • interface 可以声明合并 (Declaration Merging),即如果你在代码中多次声明同一个名字的接口,它们会自动合并为一个接口。type 别名不支持声明合并。
  • 可以描述的类型范围:
    • type 别名可以用于给原始类型、联合类型、交叉类型、元组、函数等任何类型起别名。
    • interface 主要用于描述对象的形状(包括函数类型、可索引类型)。
  • 使用建议:
    • 如果是在定义公共 API 的形状(例如库的类型定义),或者希望利用声明合并特性,通常推荐使用 interface
    • 如果需要给联合类型、交叉类型、原始类型、元组等起别名,或者需要使用一些高级类型操作(如映射类型,将在后续高级教程中介绍),则必须使用 type
    • 对于简单的对象类型,选择 interfacetype 更多是风格偏好。由于 interface 具有更好的扩展性,一些团队倾向于优先使用 interface 来定义对象结构。

联合类型 (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
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(
${this.name} moved ${distanceInMeters}m.`);
}
}

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)

类型断言用来告诉编译器你比它更清楚一个变量的实际类型。这并不是类型转换,也不会影响运行时代码。它仅仅是在编译阶段指导类型检查。

有两种形式:

  1. “尖括号”语法: <Type>value
  2. 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 = (someValue).length;
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(arg: T): T {
return arg;
}

// 调用时可以显式指定类型参数 (通常不需要,编译器会推断)
let output1 = identity(“myString”); // output1 是 string 类型

// 或者让编译器自动推断
let output2 = identity(“myString”); // output2 是 string 类型
let output3 = identity(123); // output3 是 number 类型
let output4 = identity(true); // output4 是 boolean 类型
``
通过泛型
T,我们捕获了参数arg` 的类型,并用它作为返回类型。这样,函数变得通用,同时保留了类型信息。

泛型数组

“`typescript
function loggingIdentity(arg: T[]): T[] {
console.log(arg.length); // 可以在 T[] 上使用数组属性 length
return arg;
}
// 或者使用 Array
// function loggingIdentity(arg: Array): Array {
// 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(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn = identity; // 接口指定 T 为 number
let myIdentity2: GenericIdentityFn = identity; // 接口指定 T 为 string

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(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10)); // 输出 15

let myGenericString = new GenericNumber(“”, (x, y) => x + y);
console.log(myGenericString.add(“Hello, “, “TypeScript!”)); // 输出 Hello, TypeScript!
“`

泛型约束 (Generic Constraints)

有时你希望泛型参数具备某些属性(比如 length 属性)。这时可以使用泛型约束。

“`typescript
interface Lengthwise {
length: number;
}

// 约束 T 必须实现 Lengthwise 接口
function loggingIdentity(arg: T): T {
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(obj: T, key: K) {
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 源文件的根目录。编译器会根据 rootDiroutDir 的相对路径结构来生成输出文件。
  • sourceMap: 生成 .map 文件,方便在浏览器或 Node.js 调试时映射回原始的 TypeScript 代码。
  • include: 一个文件路径数组,指定需要编译的文件或文件夹。支持 glob 模式。
  • exclude: 一个文件路径数组,指定在编译过程中需要排除的文件或文件夹。通常用于排除 node_modules 或构建输出目录。
  • files: 一个文件路径数组,精确指定需要编译的入口文件列表。如果使用了 includeexclude,通常不需要指定 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 通常不是独立使用的,而是与各种构建工具、框架和库集成。

  1. 与构建工具集成:

    • Webpack: 使用 ts-loaderawesome-typescript-loader.ts 文件作为模块加载。
    • Parcel: 对 TypeScript 有内置支持,配置简单。
    • Rollup: 使用 @rollup/plugin-typescript@rollup/plugin-babel (配置 @babel/preset-typescript)。
    • Vite: 内置支持 TypeScript,速度非常快。
  2. 与前端框架集成:

    • 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 有天然的支持。
  3. 类型声明文件 (.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
    • 如果你使用的库没有提供类型声明,你可以尝试查找社区提供的,或者自己编写。
  4. Linter 和 Formatter:

    • 使用 ESLint 结合 @typescript-eslint/eslint-plugin@typescript-eslint/parser 可以对 TypeScript 代码进行静态分析和强制代码风格。
    • Prettier 是一个流行的代码格式化工具,它也支持 TypeScript。

集成过程的核心是配置构建工具,使其能够识别和处理 .ts 文件,并利用 @types 包获取第三方库的类型定义。

结论

恭喜你!通过本文的学习,你已经了解了 TypeScript 的基础知识、核心概念以及如何在项目中使用它。我们从环境搭建开始,深入学习了基础类型、复杂类型结构、函数、接口、类型别名、联合/交叉类型、类、模块,并简要介绍了泛型、类型断言、类型守卫以及如何配置和集成 TypeScript。

TypeScript 为 JavaScript 开发带来了静态类型的强大优势,能够帮助你在开发早期捕获错误、提高代码可读性、增强可维护性,并极大地提升开发体验。虽然引入类型系统需要额外的学习成本,但从长远来看,尤其是在大型复杂项目或团队协作中,它的收益是巨大的。

下一步学习建议:

  1. 动手实践: 最好的学习方式是实践。尝试将已有的 JavaScript 项目迁移到 TypeScript,或开始一个新的 TypeScript 项目。
  2. 深入高级类型: 探索 TypeScript 的高级类型系统,如映射类型 (Mapped Types)、条件类型 (Conditional Types) 等,它们能让你处理更复杂的类型逻辑。
  3. 阅读官方文档: TypeScript 官方文档 (https://www.typescriptlang.org/docs/) 是最权威的学习资料,包含了更多详细信息和高级主题。
  4. 学习设计模式: 了解如何在 TypeScript 中应用常见的设计模式。
  5. 阅读优秀的开源项目代码: 阅读使用 TypeScript 编写的知名开源项目,学习它们如何组织代码和使用类型。
  6. 参与社区: 加入 TypeScript 社区,提问、分享经验、甚至尝试为类型声明文件做贡献。

从零开始只是第一步,TypeScript 的世界还有很多精彩等待你去探索。持续学习、不断实践,你将在构建更健壮、更可维护的应用程序的道路上越走越远!

祝你学习顺利,享受 TypeScript 带来的便利和乐趣!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部