第四部分:赋予网页健壮的灵魂 —— TypeScript(上)


我们已经让网页拥有了基本的交互能力。但是,随着网页功能越来越复杂,JavaScript 代码会变得越来越庞大。有时候我们可能会遇到一些隐藏的错误,直到运行时才会发现。这就像给智能家居系统安装了一个新的模块,结果发现线接错了,但只有在使用时才知道哪个功能坏了。

为了解决这个问题,让我们的代码更加可靠、更易于维护,我们需要一个更强大的工具。这就是 TypeScript 登场的时候了!

如果说 JavaScript 是给房子通上电、安装智能家居系统,让它“活”起来。那么 TypeScript 就像是在安装这些系统时,给我们提供了一份更详细、更规范的设计图纸和更智能的电线连接器。它不会改变最终的电力(网页运行)。但它能帮助我们在安装阶段(编写代码时)就发现潜在的问题,确保线路连接正确、系统稳定可靠。

TypeScript 是 JavaScript 的一个超集,这意味着所有合法的 JavaScript 代码也是合法的 TypeScript 代码。但 TypeScript 添加了静态类型这个概念,就像给电路中的每一根电线、每一个接口都明确规定了它的“类型”和“用途”。这使得我们可以在代码运行之前(在编写阶段或编译阶段)就发现很多潜在的类型错误,大大提高了代码的健壮性和可维护性。

1 TypeScript 初探:带标签的电线和更严谨的设计图纸

TypeScript 是一款由微软开发的开源编程语言。它在 JavaScript 的基础上增加了静态类型检查和其他一些现代语言特性。最终,TypeScript 代码会被编译成纯粹的 JavaScript 代码,才能在浏览器或 Node.js 环境中运行。你可以把这个编译过程想象成根据设计图纸(TypeScript)生产出实际的电线和设备(JavaScript)。

1.1 为什么使用 TypeScript?

  1. 提早发现错误: 大部分类型相关的错误可以在编写代码时或编译阶段就被发现,而不是等到运行时。这大大减少了调试时间。
  2. 代码更易读、易懂: 类型注解本身就是一种文档,它清晰地表明了变量、函数参数和返回值的预期类型。
  3. 更好的可维护性: 当项目规模变大、团队成员增多时,明确的类型信息使得理解和修改现有代码变得更容易、更安全。
  4. 增强开发工具体验: 支持 TypeScript 的编辑器(如 VS Code)能够提供更智能的代码补全、重构和导航功能。

1.2 安装 TypeScript

TypeScript 通常通过 npm 或 yarn 进行安装。打开你的终端或命令行工具,运行以下命令:

npm install -g typescript
# 或者使用 yarn
# yarn global add typescript

-g 参数表示全局安装,这样你就可以在任何地方使用 tsc 命令了。
在这里插入图片描述

1.3 编写和编译 TypeScript

TypeScript 文件的后缀通常是 .ts。你可以像写 JavaScript 一样开始编写代码。

先创建一个文件夹,04-typescript,在文件夹下创建第一个 TypeScript 文件,比如 01.ts

// 01.ts

// 这是一根明确标记为存放文本的电线 (string 类型)
let message: string = "你好,TypeScript!";

// 这是一根明确标记为存放数字的电线 (number 类型)
let year: number = 2025;

// 这是一根明确标记为存放真/假状态的电线 (boolean 类型)
let isReady: boolean = true;

// 你也可以让 TypeScript 自己根据赋的值推断类型 (隐式类型)
// 但对于变量,尤其是在声明时没有立即赋值的情况下,明确注解通常是更好的实践
let greeting = "Hello!"; // TypeScript 推断 greeting 是 string 类型

// 对于常量,推荐明确注解类型
const PI: number = 3.14159;

// 一些基本类型
let user: null = null; // 明确表示这是一个空值
let undefinedVar: undefined = undefined; // 明确表示这是一个未定义的值

// ES6 新增的基本类型 Symbol 和 BigInt
const uniqueId: symbol = Symbol("id");
const largeNumber: bigint = 9007199254740991n; // 注意 n 后缀

// 类型推断的例子:
let age = 30; // TypeScript 推断 age 是 number 类型

// 尝试将错误类型的值赋给有明确类型注解的变量会报错 (编译阶段)
// year = "二零二五"; // 取消注释,编译时会报错:Type 'string' is not assignable to type 'number'.

// 在控制台输出 (和 JavaScript 一样)
console.log(message);
console.log("当前年份:", year);
console.log("是否准备好?", isReady);
console.log("问候语:", greeting);
console.log("圆周率:", PI);
console.log("用户数据:", user);
console.log("未定义变量:", undefinedVar);
console.log("唯一ID:", uniqueId);
console.log("大整数:", largeNumber);
console.log("年龄:", age);

在这里插入图片描述

编写完 .ts 文件后,你需要使用 tsc 命令将其编译成 .js 文件,因为我们用到了更高阶的语法,因此需要创建一个配置文件指定我们es的版本。在04-typescrpt下创建tsconfig.json,输入如下配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020", "ESNext", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

打开终端,进入到根目录,然后运行:

tsc --project 04-typescript/tsconfig.json

这会在同一个目录下生成一个 01.js 文件,它的内容就是编译后的纯 JavaScript 代码(不包含类型注解,因为类型注解只在编译阶段有用):

// 01.js (编译后生成)

// 这是一根明确标记为存放文本的电线 (string 类型)
var message = "你好,TypeScript!";
// 这是一根明确标记为存放数字的电线 (number 类型)
var year = 2025;
// 这是一根明确标记为存放真/假状态的电线 (boolean 类型)
var isReady = true;
// 你也可以让 TypeScript 自己根据赋的值推断类型 (隐式类型)
// 但对于变量,尤其是在声明时没有立即赋值的情况下,明确注解通常是更好的实践
var greeting = "Hello!"; // TypeScript 推断 greeting 是 string 类型
// 对于常量,推荐明确注解类型
var PI = 3.14159;
// 一些基本类型
var user = null; // 明确表示这是一个空值
var undefinedVar = undefined; // 明确表示这是一个未定义的值
// ES6 新增的基本类型 Symbol 和 BigInt
var uniqueId = Symbol("id");
var largeNumber = 9007199254740991n; // 注意 n 后缀
// 类型推断的例子:
var age = 30; // TypeScript 推断 age 是 number 类型
// 尝试将错误类型的值赋给有明确类型注解的变量会报错 (编译阶段)
// year = "二零二五"; // 取消注释,编译时会报错:Type 'string' is not assignable to type 'number'.
// 在控制台输出 (和 JavaScript 一样)
console.log(message);
console.log("当前年份:", year);
console.log("是否准备好?", isReady);
console.log("问候语:", greeting);
console.log("圆周率:", PI);
console.log("用户数据:", user);
console.log("未定义变量:", undefinedVar);
console.log("唯一ID:", uniqueId);
console.log("大整数:", largeNumber);
console.log("年龄:", age);

现在,你就可以在 HTML 文件中引入这个编译后的 01.js 文件来运行你的代码了,就像之前引入 JavaScript 文件一样:

<!DOCTYPE html>
<html>
<head> <title>TS 基础</title> </head>
<body>
  <h1>查看浏览器控制台 (运行的是编译后的 JS 文件)</h1>
  <script src="01.js"></script>
</body>
</html>

运行后的效果
在这里插入图片描述

1.4 练习

  1. 编写一个 TypeScript 文件,声明三个变量:productName (string), productPrice (number), isInStock (boolean),并为它们添加明确的类型注解。赋予它们合适的值,并在控制台输出。
  2. 尝试声明一个变量 itemCount: number,然后给它赋一个布尔值 true,运行 tsc 命令,观察编译器的报错信息。
  3. 声明一个常量 APP_VERSION: string = "1.0.0";,尝试修改它的值,运行 tsc 命令,观察报错。声明一个变量 counter: number = 0;,然后将它的值增加 1,并在控制台输出。

2 核心概念:数据的形状与结构 (数组和对象)

在 JavaScript 中,我们经常需要处理列表数据(数组)和具有特定结构的数据(对象)。TypeScript 提供了强大的方式来描述这些数据的“形状”,让我们的代码更加清晰和安全。这就像在设计智能家居系统时,我们需要详细说明线缆要连接到哪些接口(数组元素的类型),以及每个设备的输入输出接口是什么样的(对象的属性和它们的类型)。

2.1 数组类型 (Array Types)

描述一个数组,你需要指定数组中所有元素的类型。有两种常用的方式:

  1. ElementType[]: 在元素类型后面加上 []
  2. Array<ElementType>: 使用泛型数组类型。

示例:

// 02.ts

// 明确指定字符串数组
let fruits: string[] = ["苹果", "香蕉", "橙子"];

// 明确指定数字数组
let primeNumbers: Array<number> = [2, 3, 5, 7, 11];

// 混合类型的数组通常使用 any[],但这不是最佳实践
// let mixedArray: any[] = [1, "hello", true];

// 推荐使用联合类型来明确表示数组可能包含哪些类型
let mixedArray: (number | string | boolean)[] = [1, "hello", true];

// 尝试向数组中添加错误类型的值会报错
// fruits.push(123); // 编译时报错:Argument of type 'number' is not assignable to parameter of type 'string'.

console.log("水果列表:", fruits);
console.log("质数列表:", primeNumbers);
console.log("混合数组:", mixedArray);

2.2 对象类型 (Object Types) 和接口 (Interfaces)

对象是 JavaScript 中非常重要的数据结构。它由一系列键值对组成。TypeScript 使用接口 (Interface) 来描述对象的“形状”——即它应该包含哪些属性,以及每个属性的类型是什么。这就像给一个智能设备定义了规格说明书,规定了它有哪些插口,每个插口是什么类型(电源、网线、音频等)。

示例:

// 02.ts (接着上面的代码)

// 定义一个接口,描述一个用户的形状
interface User {
  id: number;       // 用户ID,必须是数字
  name: string;     // 用户名,必须是字符串
  age?: number;     // 年龄,是可选的属性 (? 表示)
  isStudent: boolean; // 是否是学生,必须是布尔值
  readonly registrationDate: Date; // 注册日期,只读属性,初始化后不能修改
}

// 创建一个符合 User 接口的对象
const user1: User = {
  id: 1,
  name: "张三",
  // age 属性是可选的,可以省略
  isStudent: false,
  registrationDate: new Date()
};

// 创建另一个符合 User 接口的对象 (包含可选属性 age)
const user2: User = {
  id: 2,
  name: "李四",
  age: 25,
  isStudent: true,
  registrationDate: new Date('2023-01-15')
};

// 尝试创建一个不符合 User 接口的对象会报错
// const user3: User = { // 编译时报错:Missing properties 'id', 'name', 'isStudent' in type '{}'.
//   userName: "王五", // 编译时报错:Object literal may only specify known properties, and 'userName' does not exist in type 'User'.
//   student: true
// };

// 尝试修改只读属性会报错
// user1.registrationDate = new Date('2024-01-01'); // 编译时报错:Cannot assign to 'registrationDate' because it is a read-only property.

console.log("用户信息 1:", user1);
console.log("用户信息 2:", user2);

2.3 类型别名 (Type Aliases)

有时候,一个类型定义可能会很长或者需要在多个地方重复使用。你可以使用 type 关键字创建类型别名,给复杂的类型一个简单的名字。这就像给一个复杂的电路模块起一个方便记忆的名字。

示例:

// 02.ts (接着上面的代码)

// 定义一个类型别名,用于表示一个坐标点
type Point = {
  x: number;
  y: number;
};

// 定义一个类型别名,用于表示用户 ID (虽然简单,但语义更清晰)
type UserId = number;

// 使用类型别名来注解变量
const origin1: Point = { x: 0, y: 0 };
let currentUserId: UserId = 101;

// 可以像使用原始类型一样使用类型别名
function printPoint(p: Point) {
  console.log(`坐标: (${p.x}, ${p.y})`);
}

printPoint(origin1);

接口和类型别名都可以用来描述对象和函数的形状(函数类型我们后面会讲)。在描述对象的形状时,它们非常相似,但在某些高级用法上有所区别(比如接口可以被继承和实现类,而类型别名更灵活,可以用于联合类型、交叉类型等)。初学者可以先理解它们都能用来描述对象结构的功能。

编译 02.ts

tsc --project 04-typescript/tsconfig.json

会生成 02.js 文件。然后在 HTML 中引入 02.js
在这里插入图片描述
运行后的效果
在这里插入图片描述

小结: TypeScript 提供了数组类型、接口和类型别名来帮助我们描述数据的结构和形状,这让代码更加清晰、安全,并能利用编译器的力量提前发现错误。

2.4 练习

  1. 定义一个字符串数组 colors,包含你喜欢的几种颜色。
  2. 定义一个接口 Book,包含属性 title (string), author (string), yearPublished (number),isAvailable (boolean)。创建一个符合 Book 接口的对象。
  3. 定义一个类型别名 CallbackFunction,表示一个没有参数且没有返回值的函数类型(函数类型下一节会讲,可以先查阅资料或等下一节)。
  4. 创建一个数组 students,其元素类型是符合你定义的 User 接口的对象。

3 函数:定义操作的规范

函数是组织代码的基本单元。在 JavaScript 中,函数的参数和返回值类型是不确定的,这可能导致运行时错误。TypeScript 允许我们为函数的参数和返回值添加类型注解,就像给智能家居系统的每一个功能模块定义了明确的输入接口和输出接口,确保数据的正确流动。

3.1 函数类型注解

你可以为函数的参数和返回值明确指定类型。

// 03.ts

// 定义一个接受两个数字参数并返回一个数字的函数
function add(x: number, y: number): number {
  return x + y;
}

// 定义一个接受字符串参数没有返回值的函数 (void)
function greet(name: string): void {
  console.log(`你好,${name}!`);
}

// 使用函数
let sum = add(5, 3); // sum 的类型被推断为 number
console.log("5 + 3 =", sum);

greet("爱丽丝");

// 尝试使用错误类型的参数会报错
// add("hello", 5); // 编译时报错:Argument of type 'string' is not assignable to parameter of type 'number'.

3.2 可选参数 (?) 和默认参数

有时候函数的参数不是必需的,或者有默认值。

// 03.ts (接着上面的代码)

// 可选参数:使用 ? 标记参数
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  } else {
    return firstName;
  }
}

let name1 = buildName("张"); // 正确,lastName 是可选的
let name2 = buildName("李", "四"); // 正确

console.log("姓名1:", name1);
console.log("姓名2:", name2);

// 默认参数:为参数提供一个默认值
function setVolume(volume: number = 50): void {
  console.log(`设置音量到 ${volume}`);
}

setVolume();     // 使用默认值 50
setVolume(80);   // 使用传入的值 80

// 可选参数和默认参数不能同时使用在同一个参数上
// 可选参数必须在必需参数后面
// 默认参数不需要在必需参数后面,但通常推荐放在后面,因为如果它前面有可选参数且你省略了可选参数,你将无法为它传入值。

3.3 剩余参数 (...)

当函数需要处理不确定数量的同类型参数时,可以使用剩余参数。它会把剩余的参数收集到一个数组中。

// 03.ts (接着上面的代码)

// 接收任意数量的数字参数
function sumAll(...numbers: number[]): number {
  let total = 0;
  for (const num of numbers) {
    total += num;
  }
  return total;
}

let totalSum = sumAll(1, 2, 3, 4, 5); // numbers 会是一个 [1, 2, 3, 4, 5] 的数组
console.log("总和:", totalSum);

// 尝试传入非数字参数会报错
// sumAll(1, 2, "hello"); // 编译时报错:Argument of type 'string' is not assignable to parameter of type 'number'.

3.4 函数重载 (Function Overloads)

有时候,一个函数可能根据传入参数的类型和数量执行不同的操作,并返回不同类型的值。函数重载允许你定义多个函数签名,但只有一个函数实现。这就像一个多功能的遥控器按钮,按下一次是开灯,按下两次是调节亮度,按下三次是关闭,但它们都由同一个物理按钮触发。

// 03.ts (接着上面的代码)

// 函数重载签名 (只定义类型,没有实现)
function create(name: string): User; // 如果传入字符串,返回 User 对象
function create(id: number): User;    // 如果传入数字,返回 User 对象
function create(name: string, age: number): User; // 如果传入字符串和数字,返回 User 对象

// 函数实现 (使用 any 或联合类型处理所有可能的输入)
// 实现体的参数和返回值类型应该兼容所有重载签名
function create(nameOrId: string | number, age?: number): User {
  if (typeof nameOrId === 'string') {
    if (age !== undefined) {
        // 处理 name 和 age 的情况
        return { id: Math.random(), name: nameOrId, age: age, isStudent: false, registrationDate: new Date() };
    } else {
        // 处理只有 name 的情况
        return { id: Math.random(), name: nameOrId, isStudent: false, registrationDate: new Date() };
    }
  } else {
    // 处理只有 id 的情况
    return { id: nameOrId, name: `用户${nameOrId}`, isStudent: false, registrationDate: new Date() };
  }
}

// 使用重载函数
const userA = create("赵六"); // TypeScript 知道 userA 是 User 类型
const userB = create(102);   // TypeScript 知道 userB 是 User 类型
const userC = create("钱七", 30); // TypeScript 知道 userC 是 User 类型

console.log("用户 A:", userA);
console.log("用户 B:", userB);
console.log("用户 C:", userC);

// 尝试传入不符合任何重载签名的参数会报错
// create(true); // 编译时报错:No overload matches this call.

重要提示: 函数重载的实现体是不对外可见的,只用于 TypeScript 在编译阶段进行类型检查。调用函数时,TypeScript 会根据你传入的参数去匹配上面的重载签名,找到最匹配的一个来确定调用是否合法以及返回值的类型。实现体的类型注解 (nameOrId: string | number, age?: number): User) 必须能兼容所有的重载签名。

3.5 箭头函数类型

箭头函数也可以使用类型注解,语法与普通函数类似:

// 03.ts (接着上面的代码)

// 定义一个接受两个数字参数并返回数字的箭头函数
const subtract: (a: number, b: number) => number = (a, b) => a - b;

// 或者更简洁地让 TypeScript 推断参数类型 (但返回值类型最好明确)
const multiply = (a: number, b: number): number => a * b;

console.log("10 - 4 =", subtract(10, 4));
console.log("6 * 7 =", multiply(6, 7));

编译 03.ts

tsc --project 04-typescript/tsconfig.json

会生成 03.js 文件。然后在 HTML 中引入 03.js

运行后的效果
在这里插入图片描述

小结: TypeScript 为函数提供了强大的类型注解能力,包括参数、返回值、可选/默认/剩余参数以及函数重载,这使得函数的输入输出更加明确,减少了因参数类型错误导致的运行时问题。

3.6 练习

  1. 编写一个函数 calculateArea,接受矩形的宽度和高度 (都是 number 类型),返回计算出的面积 (number 类型)。
  2. 编写一个函数 logMessage,接受一个字符串参数 message,一个可选的布尔值参数 isError (默认为 false)。如果 isError 为 true,则在控制台以错误级别输出 (console.error),否则以信息级别输出 (console.log)。
  3. 编写一个函数 getFirstElement,接受任意类型的数组作为参数,返回数组的第一个元素(如果数组为空则返回 undefined)。使用函数重载来处理传入 string[] 时返回 string | undefined,传入 number[] 时返回 number | undefined 的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

低代码布道师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值