我们都知道,一旦把数组定义为 “arr: number[]”,那么 arr 里面就只能存放数字。而如果将数组定义为 “arr: string[]”,那么 arr 里面就只能存放字符串。
在实际开发中,偶尔会遇到一种特殊的需求:我们需要一个容器,它里面既有数字,又有字符串,而且各个位置上的数据类型都是严格规定好的。比如,我们想用一个数组来表示一个人的基础信息,规定第 1 个元素必须是姓名(string),第 2 个元素必须是年龄(number)。
像这样的需求,普通的 TypeScript 数组就显得无能为力了。这个时候,我们就需要借助 “TypeScript 元组” 才行。
TypeScript 元组是什么?
在 TypeScript 中,元组(Tuple)本质上就是一个 “长度固定、且各元素类型严格规定” 的特殊数组。
语法:
const 变量名: [类型1, 类型2, ...] = [值1, 值2, ...];说明:
元组可以允许我们在一个数组中存放不同类型的数据,但前提是必须事先 “排好座位”,谁放哪个位置、是什么类型,都得先安排得清清楚楚。
示例 1:定义基本元组
const userInfo: [string, number] = ["Jack", 20];
console.log(userInfo[0]);
console.log(userInfo[1]);运行结果如下。
Jack
20分析:
从语法上看,元组和数组非常像,小伙伴们可以对比理解一下。
// 数组
const colors: string[] = ["red", "green", "blue"];
// 元组
const userInfo: [string, number] = ["Jack", 20];对于这个例子来说,如果我们没有严格按照 [string, number] 这个类型,比如改变顺序写成:
const userInfo: [string, number] = [20, "Jack"];此时 TypeScript 编译器立马就会飘红报错,如下图所示。

提示: Python 也有 “元组” 的概念,学过 Python 的小伙伴们可以对比理解一下。
TypeScript 元组的可选元素
在 TypeScript 中,元组也是可以比较灵活的。如果元组里的某些元素不一定每次都有,我们可以使用问号 “?” 将其标记为可选元素。
示例 2:包含可选元素的元组
// 第 3 个元素是可选的布尔值
const user1: [string, number, boolean?] = ["Jack", 20, true];
const user2: [string, number, boolean?] = ["Lucy", 25];
console.log(user1.length);
console.log(user2.length);运行结果如下。
3
2分析:
在类型后面加上 “?”,表示的是这个位置的值是可选的。也就是说,这个位置可以有值,也可以没有值。细心的小伙伴可能发现了:包含可选元素的元组,它的长度是不固定的。比如 user1 长度是 3,而 user2 长度是 2。
此外,有一点我们必须注意:可选元素必须放在元组的最后面,不能把它夹在必选元素的前面或中间,否则就会报错:
// 正确
const user: [string, number, boolean?] = ["Jack", 20, true];
// 错误
const user: [string, boolean?, number] = ["Jack", true, 20]; TypeScript 元组的越界
在 TypeScript 中,由于元组规定了长度和各个位置的类型,因此当我们访问和修改时,会受到严格的限制。
示例 3:元组的越界
const userInfo: [string, number] = ["Jack", 20];
userInfo[0] = "Lucy";
console.log(userInfo)运行结果如下。
[ 'Lucy', 20 ]分析:
由于我们限制了元组第 2 个元素的类型为 “string”,因此想要修改第 2 个元素的值,新值的类型必须是 “string” 才行。如果使用下面这样的代码,则会直接提示报错:
// 尝试修改第 1 个元素
userInfo[0] = 666;
// 报错:不能将类型 “number” 分配给类型 “string”此外,由于定义元组时只规定了 2 个坑位,如果我们试图去访问或修改第 3 个坑位(索引为 2),也是非法的,比如:
// 尝试修改第 3 个元素
userInfo[2] = true;
// 报错:不能将类型 “true” 分配给类型 “undefined”示例 4:push() 漏洞
const userInfo: [string, number] = ["Jack", 20];
userInfo.push(666);
console.log(userInfo)运行结果如下。
[ 'Jack', 20, 666 ]分析:
虽然使用 userInfo[2] = true; 这种通过索引越界修改的方式,会被 TypeScript 严格拦截。但由于元组本质上依然是数组,如果我们对其使用 push() 方法(例如 userInfo.push(666)),只要被追加的数据类型属于元组中已存在的类型(在这个例子中就是 string 或 number),TypeScript 就不会拦截报错并能正常运行。这种现象被称为 “元组的越界漏洞”。
在实际开发中,如果想要彻底锁死元组,不让它被 push(),我们可以使用 readonly 修饰符来限制:
const userInfo: readonly [string, number] = ["Jack", 20];TypeScript 元组的应用
很多初学的小伙伴看到这里可能会疑惑:“元组这东西看起来比较死板,平时真的用得上吗?”
实际上,元组在现代前端开发(特别是 React 和 Vue 3 的 Composition API 中)应用是非常广泛的。它最经典的应用场景就是:封装自定义 Hook(或组合式函数),以及规范 API 返回的轻量级结构数据。
示例 5:模拟 API 响应数据
const response: [number, string] = [200, "请求成功"];
const statusCode = response[0];
const message = response[1];
console.log("状态码:", statusCode);
console.log("提示:", message);运行结果如下。
状态码:200
提示:请求成功分析:
像 “[状态码, 提示信息]” 这样的数据结构,如果我们为了它专门去定义一个完整的对象,难免有点 “杀鸡用牛刀” 的感觉。但使用元组则简单方便多了。
如果小伙伴们学过 React,肯定对 useState() 非常熟悉:
const [count, setCount] = useState(0);实际上,useState() 底层返回的就是一个元组!它严格规定了第 1 个元素是状态值,第 2 个元素是修改状态的函数。
TypeScript 具名元组(标签元组)
在前面定义 API 响应的例子中,我们使用了 “[number, string]”。虽然类型是限制住了,但如果别的同事来看你的代码,他光看类型依然不知道这个 number 代表的到底是 “状态码”,还是 “用户 ID” 或其他。
为了提高代码的可读性,TypeScript 4.0 引入了 “具名元组” 的语法。
语法:
const 变量名: [标签名1: 类型1, 标签名2: 类型2, ...] = [值1, 值2, ...];说明:
具名数组,允许我们给元组的每个坑位打上 “标签”,从而提高代码的可读性和可维护性。
示例 6:具名元组的定义
// 为每个类型加上 “标签名:”
type ApiResponse = [code: number, message: string];
const response: ApiResponse = [200, "请求成功"];分析:
在这个例子中,“code:” 和 “message:” 仅仅是供人(以及编辑器)看的提示符,在 TypeScript 编译后会完全消失,而不会影响代码本身。
不过当我们把鼠标悬停在变量上,或者调用这个元组时,编辑器会非常智能地提示你第 1 个参数是 code,第 2 个是 message。
