TypeScript: TypeScript 数组与元组
最后更新:2026-08-26
数组和元组是 TypeScript 中最常用的两种集合类型——数组是"同类型的一组值",元组是"固定长度、每项有特定类型"的特殊数组。
1. 数组类型
(1) 两种注解语法
TypeScript 有两种等价的数组类型写法:
TYPESCRIPT
// 写法一:类型 + 方括号(推荐,更直观)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Charlie", "Diana", "Eric"];
// 写法二:泛型数组(常用在泛型场景)
let scores: Array<number> = [90, 85, 95];
let items: Array<string> = ["a", "b", "c"];
📌 建议: 日常开发用
类型[] 写法,更简洁。泛型写法 Array<T> 在某些复杂类型或与泛型函数配合时更清晰。
(2) 数组类型推断
TYPESCRIPT
let arr1 = [1, 2, 3]; // 推断为 number[]
let arr2 = ["a", "b", "c"]; // 推断为 string[]
let arr3 = [1, "hello", true]; // 推断为 (number | string | boolean)[]
let arr4 = []; // 推断为 any[](危险!)
🔥 易错: 空数组
[] 推断为 any[],后续赋值不同类型也不会报错。严格模式下建议显式标注:let arr4: number[] = []。
(3) 多维数组
TYPESCRIPT
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 访问元素
console.log(matrix[0][1]); // 2
// 三维数组
let cube: number[][][] = [[[1]]];
▶ 示例:数组基本操作
TYPESCRIPT
let fruits: string[] = ["苹果", "香蕉", "橘子"];
// 添加元素
fruits.push("葡萄"); // 末尾添加
fruits.unshift("草莓"); // 开头添加
// 删除元素
let last = fruits.pop(); // 删除末尾,返回 "葡萄"
let first = fruits.shift(); // 删除开头,返回 "草莓"
// 查找
let index = fruits.indexOf("香蕉"); // 0(草莓和葡萄被删了,香蕉变成第一个)
// 遍历
fruits.forEach((fruit, i) => {
console.log(`${i}: ${fruit}`);
});
输出:
TEXT
📖 仅展示
0: 香蕉
1: 橘子
2. 只读数组
只读数组不能被修改——添加、删除、修改元素都不允许:
(1) readonly 修饰符
TYPESCRIPT
let readonlyArr: readonly number[] = [1, 2, 3];
// readonlyArr[0] = 10; // ❌ 只读,不能修改
// readonlyArr.push(4); // ❌ 只读,不能添加
// readonlyArr.length = 0; // ❌ 只读,不能改长度
let normalArr: number[] = [1, 2, 3];
readonlyArr = normalArr; // ✅ 普通数组可以赋给只读数组
// normalArr = readonlyArr; // ❌ 只读数组不能赋给普通数组
(2) ReadonlyArray 泛型
TYPESCRIPT
let readonlyArr2: ReadonlyArray<string> = ["a", "b", "c"];
// 和 readonly string[] 等价
💡 用途: 函数参数用只读数组,明确告诉调用者"这个函数不会修改你的数组":
TYPESCRIPT
function printItems(items: readonly string[]) {
items.forEach(item => console.log(item));
// items.push("new"); // ❌ 编译错误——函数承诺不修改
}
let myItems = ["a", "b"];
printItems(myItems); // 安全——myItems 不会被修改
3. 元组类型(Tuple)
元组是"固定长度、每项有特定类型"的数组——可以理解为"有名字的固定结构"的轻量版。
(1) 基本语法
TYPESCRIPT
// 定义元组:[类型1, 类型2, ...]
let user: [string, number] = ["Charlie", 20];
// 按索引访问
console.log(user[0]); // "Charlie"(string 类型)
console.log(user[1]); // 20(number 类型)
// 赋值必须匹配类型和长度
// user = [20, "Charlie"]; // ❌ 类型顺序不匹配
// user = ["Charlie"]; // ❌ 长度不够
// user = ["Charlie", 20, true]; // ❌ 超出长度
(2) 元组 vs 数组的区别
| 特性 | 数组 number[] |
元组 [string, number] |
|---|---|---|
| 长度 | 任意 | 固定 |
| 元素类型 | 全部相同 | 每个位置可以不同 |
| 访问 | 所有元素同一类型 | 每个索引有特定类型 |
| 适用场景 | 同类数据集合 | 固定结构的小数据 |
(3) 元组解构
TYPESCRIPT
let pair: [string, number] = ["苹果", 5];
let [fruit, count] = pair;
console.log(fruit); // "苹果"
console.log(count); // 5
// 带剩余元素
let record: [string, number, ...boolean[]] = ["测试", 100, true, false, true];
let [name, score, ...flags] = record;
console.log(name); // "测试"
console.log(score); // 100
console.log(flags); // [true, false, true]
(4) 可选元素
元组中可以用 ? 标记可选元素:
TYPESCRIPT
let result: [string, number?] = ["成功"];
console.log(result[0]); // "成功"
console.log(result[1]); // undefined(可选元素可以不提供)
⚠️ 注意: 可选元素只能在末尾。
[string?, number] 是非法的——可选元素之后不能再有必选元素。
(5) 带标签的元组
TypeScript 4.0 引入了元组标签,让每个位置的语义更清晰:
TYPESCRIPT
let user: [name: string, age: number] = ["Charlie", 20];
let point: [x: number, y: number, z?: number] = [1.0, 2.5];
// 标签只是文档作用,不影响类型——但代码可读性大大提升
function getRange(): [min: number, max: number] {
return [0, 100];
}
let [min, max] = getRange();
console.log(`范围:${min} - ${max}`); // 范围:0 - 100
▶ 示例:元组表示函数返回值
TYPESCRIPT
// 元组非常适合表示函数返回的"多值"
function divide(a: number, b: number): [quotient: number, remainder: number] {
return [Math.floor(a / b), a % b];
}
let [quotient, remainder] = divide(17, 5);
console.log(`商:${quotient},余数:${remainder}`); // 商:3,余数:2
// 元组表示键值对
function entries(obj: Record<string, number>): [string, number][] {
return Object.entries(obj) as [string, number][];
}
let scores = { 语文: 90, 数学: 95, 英语: 88 };
for (let [subject, score] of entries(scores)) {
console.log(`${subject}:${score}分`);
}
输出:
TEXT
📖 仅展示
商:3,余数:2
语文:90分
数学:95分
英语:88分
4. 数组的高阶类型操作
(1) 联合类型数组 vs 数组的联合类型
TYPESCRIPT
// 联合类型数组:数组中每个元素可以是 number 或 string
let mixed: (number | string)[] = [1, "two", 3, "four"];
// 数组的联合类型:要么是 number[],要么是 string[]
let uniform: number[] | string[] = [1, 2, 3]; // ✅
uniform = ["a", "b", "c"]; // ✅
// uniform = [1, "two"]; // ❌ 不能混合
🔥 易错:
(number | string)[] 和 number[] | string[] 含义完全不同!前者是"混合数组",后者是"纯数字数组或纯字符串数组"。
(2) 数组方法的类型安全
TYPESCRIPT
let nums: number[] = [3, 1, 4, 1, 5];
// sort 需要显式比较函数,否则按字符串排序
nums.sort((a, b) => a - b); // ✅ 数字排序:[1, 1, 3, 4, 5]
// map 自动推断返回类型
let doubled = nums.map(n => n * 2); // 推断为 number[]
let asStrings = nums.map(String); // 推断为 string[]
// filter 自动推断元素类型
let big = nums.filter(n => n > 2); // 推断为 number[]
(3) 数组展开与类型
TYPESCRIPT
let a: number[] = [1, 2];
let b: number[] = [3, 4];
let merged: number[] = [...a, ...b]; // [1, 2, 3, 4]
// 元组展开
let prefix: [string, number] = ["序号", 0];
let full: [string, number, ...number[]] = [...prefix, 1, 2, 3];
// full = ["序号", 0, 1, 2, 3]
5. 实际开发中的选择
(1) 什么时候用数组
- 同类数据的集合(用户列表、分数列表、配置项列表)
- 不确定元素数量
- 需要遍历、查找、排序等操作
(2) 什么时候用元组
- 固定长度、固定结构的小数据(坐标
[x, y]、键值对[key, value]) - 函数返回多个值
- 只有两三个元素的简单结构(更复杂的用对象或接口)
(3) 什么时候用只读数组
- 函数参数——承诺不修改传入的数组
- 常量配置——防止意外修改
- API 返回值——告诉使用者数据不可变
▶ 示例:综合练习——学生成绩管理
TYPESCRIPT
// 用元组表示 [科目, 分数]
type Score = [subject: string, score: number];
// 用数组存储多个成绩
let scores: Score[] = [
["语文", 92],
["数学", 88],
["英语", 95],
["物理", 78],
["化学", 85]
];
// 计算平均分
function average(scores: readonly Score[]): number {
let total = scores.reduce((sum, [, score]) => sum + score, 0);
return total / scores.length;
}
// 找最高分科目
function topSubject(scores: readonly Score[]): Score {
return scores.reduce((best, current) =>
current[1] > best[1] ? current : best
);
}
console.log("平均分:" + average(scores).toFixed(1));
let [subject, score] = topSubject(scores);
console.log(`最高分科目:${subject}(${score}分)`);
输出:
TEXT
📖 仅展示
平均分:87.6
最高分科目:英语(95分)
❓ 常见问题
Q 元组和普通对象有什么区别?什么时候用哪个?
A 元组通过索引访问(
tuple[0]),对象通过键访问(obj.name)。元组适合2-3个元素的简单固定结构(如坐标、键值对);超过3个元素或语义复杂时用对象/接口,因为 user.name 比 user[0] 可读得多。Q (number | string)[] 和 number[] | string[] 到底有什么区别?
A
(number | string)[] 是"每个元素可以是 number 或 string"的数组——允许混合如 [1, "a", 2]。number[] | string[] 是"整个数组要么全是 number,要么全是 string"——不允许混合,[1, "a"] 不合法。后者是两个数组类型的联合,不是联合类型数组。Q readonly number[] 和 number[] 的赋值关系是什么?
A
number[] 可以赋给 readonly number[](变严格是安全的),但 readonly number[] 不能赋给 number[](变宽松会失去只读保护)。这和 const 的赋值方向类似——只能从宽松到严格。Q 元组的 push/pop 为什么不被类型检查?
A 这是 TypeScript 的一个已知设计缺陷。元组的越界元素访问(如
tuple[100])会报错,但 push/pop 方法在严格模式下可以执行且不被类型检查。建议用 readonly 元组(readonly [string, number])来彻底禁止修改操作。📖 小节
- 数组类型有两种写法:
类型[](推荐)和Array<类型>,效果完全相同 readonly 类型[]和ReadonlyArray<类型>创建只读数组,不能增删改- 元组
[类型1, 类型2, ...]是固定长度、每项有特定类型的特殊数组 - 元组支持解构、可选元素(
类型?)、剩余元素(...类型[])和标签([name: string]) (A | B)[](联合类型数组)和A[] | B[](数组的联合类型)含义完全不同
📝 作业
- 基础题(难度⭐):创建一个
string[]数组存放5种编程语言名,用forEach遍历输出。然后写一个只读版本readonly string[],尝试修改并观察报错。 - 进阶题(难度⭐⭐):用元组定义函数
swap(tuple: [string, number]): [number, string],交换元组中两个元素的位置和类型。例如swap(["hello", 42])返回[42, "hello"]。 - 挑战题(难度⭐⭐⭐):实现一个函数
parseCSV(lines: string[]): [header: string[], ...rows: string[][]],把 CSV 文本行解析为元组——第一行是表头,其余行是数据。返回类型用带剩余元素的标签元组。