在 TypeScript 中,数组(Array)和元组(Tuple)最大的区别是:数组通常只约束元素类型,而元组还可以约束元素的数量、顺序以及每个位置的类型。
你之前主要写 Go 后端,我们直接结合 Go 的数组、切片和结构体来理解。
一、先看数组(Array)
TypeScript 数组的写法:
const numbers: number[] = [1, 2, 3];
const names: string[] = ["张三", "李四", "王五"];
解释:
number[]:数组元素必须是数字。string[]:数组元素必须是字符串。- 数组长度可以动态变化。
例如:
const numbers: number[] = [1, 2, 3];
numbers.push(4); // 正确
numbers.push(5); // 正确
numbers.push("6"); // 类型错误
这里 numbers 最终变成:
[1, 2, 3, 4, 5]
你可以把 TypeScript 数组类比为 Go 的切片:
numbers := []int{1, 2, 3}
numbers = append(numbers, 4)
注意:TypeScript 的 Array 更接近 Go 的 slice,而不是 Go 的固定长度数组 [3]int。
二、再看元组(Tuple)
元组的写法:
const user: [number, string] = [1001, "张三"];
解释:
[number, string]
规定了:
- 第一个元素必须是
number。 - 第二个元素必须是
string。 - 这个元组正常赋值时必须有两个元素。
例如:
const user: [number, string] = [1001, "张三"]; // 正确
const user2: [number, string] = ["张三", 1001]; // 错误:顺序不对
const user3: [number, string] = [1001]; // 错误:缺少元素
const user4: [number, string] = [1001, "张三", 18]; // 错误:元素过多
所以你可以理解成:
元组就是对数组中的每个位置都规定了类型的特殊数组。
三、数组和元组的直观对比
数组 Array
number[]
元素 1
10
number
元素 2
20
number
元素 3
30
number
元素 4
40
number
长度可变,元素类型相同
元组 Tuple
[number, string]
位置 0
1001
number
位置 1
张三
string
位置和类型都有约束
四、一个容易误解的地方:数组也能存不同类型
例如:
const data: (number | string)[] = [1001, "张三", 18];
这也是合法的数组。
那么和元组有什么区别?
// 数组
const data: (number | string)[] = [1001, "张三"];
// 元组
const user: [number, string] = [1001, "张三"];
对比:
data[0] = "李四"; // 正确
data[1] = 2002; // 正确
user[0] = "李四"; // 类型错误
user[1] = 2002; // 类型错误
原因:
- 数组
(number | string)[]:任意位置都可以是数字或者字符串。 - 元组
[number, string]:第一个位置必须是数字,第二个位置必须是字符串。
五、元组的典型应用:函数返回多个值
你写过 Go,应该比较熟悉多返回值:
func getUser() (int64, string) {
return 1001, "张三"
}
func main() {
id, name := getUser()
fmt.Println(id, name)
}
TypeScript 没有 Go 那样的原生多返回值机制,但可以通过元组实现类似效果:
function getUser(): [number, string] {
return [1001, "张三"];
}
const [id, name] = getUser();
console.log(id); // 1001
console.log(name); // 张三
这里:
const [id, name] = getUser();
叫作数组解构赋值。
你可以理解为从元组中取出第一个和第二个元素,分别赋值给 id 和 name。
六、元组还有一些特殊用法
1. 可选元素
let user: [number, string?];
user = [1001]; // 正确
user = [1001, "张三"]; // 正确
string? 表示第二个元素可以不存在。
2. 带名称的元组
type User = [id: number, name: string];
const user: User = [1001, "张三"];
这里的 id 和 name 只是提高可读性的标签,并不会把元组变成对象。
访问方式仍然是:
console.log(user[0]); // 1001
console.log(user[1]); // 张三
3. readonly 元组
还有一个容易踩坑的地方:普通元组在运行时仍然是 JavaScript 数组。
例如:
let user: [number, string] = [1001, "张三"];
user.push("李四"); // TypeScript 允许
因此,普通元组并不等于运行时不可改变的固定长度数组。
如果不希望通过 push 修改,可以使用:
const user: readonly [number, string] = [1001, "张三"];
user.push("李四"); // 类型错误
readonly 是 TypeScript 的编译期约束,并不自动冻结运行时对象。
七、最后总结
| 对比 | 数组 Array | 元组 Tuple |
|---|---|---|
| 定义方式 | number[] | [number, string] |
| 元素类型 | 通常统一,也可以是联合类型 | 可以按位置指定不同类型 |
| 元素顺序 | 不限制各位置的具体类型 | 有类型顺序约束 |
| 长度 | 通常可变 | 类型层面可约束长度 |
| 使用场景 | 数据列表、批量记录 | 固定结构、函数多个返回值 |
| Go 类比 | []int 切片 | 类似固定结构的多返回值 |
实际开发建议:
- 一批任务 ID:使用
number[]。 - 一批用户名称:使用
string[]。 - 一个函数需要返回
[任务ID, 是否成功]:可以使用[number, boolean]。 - 一个用户包含很多字段:通常优先使用
interface或type定义对象,而不是使用很长的元组。
一句话记忆:数组关注“装什么类型的数据”,元组关注“每个位置应该放什么数据”。