从开发痛点到实战:彻底搞懂 TypeScript 数组与元组
一、数组(Array):同类数据
先思考一个场景:你需要存储一个班级所有学生的成绩,这些数据的特点是"类型相同(都是数字)、数量不固定(可能新增转学生)"。这时,数组就是最佳选择。
1. 核心定位:动态的同类型集合
数组的核心作用是存储相同类型的有序数据,长度可以动态调整(增删元素),就像一个可以无限扩容的收纳盒,专门用来装"同类物品"。TypeScript 通过类型标注强制数组内元素类型统一,避免混入其他类型导致错误。
2. 案例:解决同类数据存储问题
① 基础用法:明确类型的数组
最常用的两种类型标注方式:类型[] 和 Array<类型>,效果完全一致,前者更简洁。
ts
// 方式1:类型[](推荐)
const scores: number[] = [90, 85, 95, 88];
// 支持动态增删
scores.push(92); // 新增成绩
scores.pop(); // 删除最后一个成绩
console.log(scores.length); // 输出:4
// 方式2:Array<类型>(泛型写法)
const studentNames: Array<string> = ["张三", "李四", "王五"];
// 数组方法有完整类型提示
const filteredNames = studentNames.filter(name => name.length > 2);
console.log(filteredNames); // 输出:["张三"]
② 慎用场景:混合类型的 any 数组
有些同学会用 Array<any> 存储不同类型的数据,但这会完全丧失 TypeScript 的类型校验能力,相当于"放弃治疗",仅在兼容旧 JS 代码的特殊场景临时使用。
ts
// 不推荐!失去类型安全,TS 无法校验元素类型
const mixedData: any[] = [123, "hello", true, { id: 1 }];
// 取值时无类型提示,容易出错
const num = mixedData[0] + 10; // 没问题
const str = mixedData[1].toUpperCase(); // 没问题
const err = mixedData[2].slice(0, 1); // 运行时错误:boolean 没有 slice 方法
③ 进阶用法:多维数组(嵌套数组)
当需要存储"同类数据的集合的集合"时,就需要用到多维数组,比如矩阵、表格数据。
ts
// 二维数字数组:表示一个 3x3 的矩阵
const matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 访问二维数组元素:行索引 + 列索引
console.log(matrix[1][2]); // 输出:6(第二行第三列)
// 遍历二维数组
matrix.forEach(row => {
const sum = row.reduce((acc, curr) => acc + curr, 0);
console.log(`行总和:${sum}`);
});
二、元组(Tuple):异构数据的"固定结构包"
再看一个场景:你需要存储一个用户的核心信息------姓名(字符串)、年龄(数字)、是否VIP(布尔值)。这些数据的特点是"类型不同、数量固定",用数组存储会丢失类型语义,用对象又略显繁琐。这时,元组就能完美解决。
1. 核心定位:固定长度的异构组合
元组是 TypeScript 对 JavaScript 数组的扩展,核心作用是存储不同类型的有序数据,且长度固定(定义时约定多少个元素,就必须有多少个元素)每个索引位置的类型都是明确约定的,顺序具有严格语义。
2.案例:解决异构数据结构化问题
① 基础用法:固定结构的元组
元组的类型标注格式为 [类型1, 类型2, ...],每个位置的类型可以不同,且必须严格匹配。
ts
// 元组:[姓名(string), 年龄(number), 是否VIP(boolean)]
const user: [string, number, boolean] = ["张三", 25, true];
console.log(user[0]); // 输出:张三
console.log(user[1]); // 输出:25
// 错误示例:类型不匹配或长度不符都会报错
// user = ["李四", "30", true]; // 报错:第二个元素应为 number
// user = ["王五", 30]; // 报错:长度应为 3
② 使用场景:函数返回多个不同类型值
JavaScript 函数默认只能返回一个值,要返回多个值通常需要用对象包裹。而元组可以更简洁地表达多返回值的类型,配合解构赋值。
ts
// 函数返回元组:[是否成功(boolean), 数据(string[]), 错误信息(string)]
function fetchUserList(): [boolean, string[], string] {
try {
// 模拟接口请求成功
const data = ["张三", "李四", "王五"];
return [true, data, ""];
} catch (error) {
// 模拟接口请求失败
return [false, [], "请求失败:网络错误"];
}
}
// 解构赋值:语义清晰,类型提示完整
const [success, userList, errorMsg] = fetchUserList();
if (success) {
console.log("用户列表:", userList);
} else {
console.error(errorMsg);
}
③ 进阶用法:元组类型别名 + 元组数组
当需要批量处理多个相同结构的元组时,可以先定义元组类型别名,再创建元组数组,大幅提升代码可读性和可维护性。
ts
// 定义用户接口
interface User {
name: string;
age: number;
}
// 模拟异步获取用户数据的函数
function fetchUsers(): Promise<User[]> {
return new Promise((resolve, reject) => {
const success = true; // 假设请求成功
if (success) {
const users: User[] = [
{ name: "张三", age: 18 },
{ name: "李四", age: 15 },
{ name: "王五", age: 25 },
];
resolve(users);
} else {
reject("请求失败:网络错误");
}
});
}
// 函数返回用户信息和状态
async function loadAdultUsers(): Promise<[boolean, User[], string]> {
try {
const users = await fetchUsers();
// 过滤出成年用户(年龄大于等于18)
const adultUsers = users.filter(user => user.age >= 18);
return [true, adultUsers, ""];
} catch (error) {
return [false, [], error as string];
}
}
// 使用示例
(async () => {
const [success, adultUsers, errorMsg] = await loadAdultUsers();
if (success) {
console.log("成年用户列表:", adultUsers);
} else {
console.error(errorMsg);
}
})();
三、核心差异
混淆数组和元组,本质是没抓住核心差异。下面这张表从 6 个关键维度对比,一看就懂:
| 对比维度 | 数组(Array) | 元组(Tuple) |
|---|---|---|
| 长度特性 | 动态可变,支持增删元素 | 固定长度,与定义时的类型数量严格匹配 |
| 元素类型 | 推荐同类型(异构需用 any,丧失类型安全) | 支持异构类型,每个索引类型可自定义 |
| 类型安全 | 仅校验元素类型,不校验长度 | 同时校验元素类型和长度,安全性更高 |
| 语义表达 | 表示"同类事物的列表"(如所有学生成绩、所有商品名称) | 表示"不同类型数据的组合"(如姓名+年龄、是否成功+数据) |
| 使用成本 | 低,上手简单,无需关注索引语义 | 中,需提前约定索引语义,避免顺序错乱 |
| 扩展能力 | 强,支持数组所有原生方法(map、filter、reduce 等) | 弱,本质是数组,但固定长度限制了部分扩展场景 |
核心结论:数组的核心是"动态+同类",元组的核心是"固定+异构"。记住这八个字,就能快速判断该用哪个。