Skip to content

TypeScript 元组

在 TypeScript 中,元组(Tuple)是一种特殊的数组类型,它允许存储不同类型的元素,并且长度是固定的。元组是 TypeScript 特有的类型,它提供了一种类型安全的方式来表示具有固定结构的数据。本文将详细介绍 TypeScript 中的元组类型。

1. 基本用法

声明元组

元组的声明使用方括号 [],并在其中指定每个元素的类型。

typescript
// 声明一个包含字符串和数字的元组
let person: [string, number] = ["John", 30];

// 声明一个包含多个类型的元组
let employee: [number, string, boolean] = [1, "John", true];

// 声明一个空元组
let empty: [] = [];

访问元组元素

元组的元素可以通过索引访问,与数组类似。

typescript
let person: [string, number] = ["John", 30];

console.log(person[0]); // 输出:John
console.log(person[1]); // 输出:30

修改元组元素

元组的元素可以通过索引修改,但是类型必须与声明时的类型一致。

typescript
let person: [string, number] = ["John", 30];

person[0] = "Jane"; // 正确:类型匹配
person[1] = 25; // 正确:类型匹配

// 错误:类型不匹配
// person[0] = 123; // 类型 'number' 不能赋值给类型 'string'
// person[1] = "30"; // 类型 'string' 不能赋值给类型 'number'

元组的长度

元组的长度是固定的,不能添加或删除元素。

typescript
let person: [string, number] = ["John", 30];

console.log(person.length); // 输出:2

// 错误:元组长度固定,不能添加元素
// person.push("Engineer"); // 类型 '[string, number]' 上不存在属性 'push'

// 错误:元组长度固定,不能删除元素
// person.pop(); // 类型 '[string, number]' 上不存在属性 'pop'

2. 类型注解

基本类型元组

typescript
// 字符串和数字的元组
let person: [string, number] = ["John", 30];

// 布尔值和字符串的元组
let flag: [boolean, string] = [true, "active"];

// 数字和数字的元组
let coordinates: [number, number] = [10, 20];

复杂类型元组

typescript
// 对象和数组的元组
let data: [object, number[]] = [{ name: "John" }, [1, 2, 3]];

// 函数和字符串的元组
let funcTuple: [() => void, string] = [() => console.log("hello"), "message"];

// 接口和联合类型的元组
interface User { id: number; name: string; }
let userTuple: [User, string | number] = [{ id: 1, name: "John" }, "admin"];

可选元素

元组可以包含可选元素,使用 ? 标记。

typescript
// 可选元素的元组
let person: [string, number, string?] = ["John", 30];
let personWithTitle: [string, number, string?] = ["John", 30, "Engineer"];

// 多个可选元素
let data: [string, number?, boolean?] = ["test"];
let dataWithNumber: [string, number?, boolean?] = ["test", 123];
let dataWithAll: [string, number?, boolean?] = ["test", 123, true];

剩余元素

元组可以包含剩余元素,使用 ... 标记。

typescript
// 剩余元素的元组
let numbers: [number, ...number[]] = [1, 2, 3, 4, 5];
let mixed: [string, ...number[]] = ["test", 1, 2, 3];
let anyRest: [string, ...any[]] = ["test", 1, true, { name: "John" }];

泛型元组

typescript
// 泛型元组
function createPair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

let pair1 = createPair("hello", 123); // 类型为 [string, number]
let pair2 = createPair(true, { name: "John" }); // 类型为 [boolean, { name: string }]

3. 元组操作

解构赋值

元组可以使用解构赋值来提取元素。

typescript
let person: [string, number] = ["John", 30];

// 解构赋值
let [name, age] = person;
console.log(name); // 输出:John
console.log(age); // 输出:30

// 解构部分元素
let [nameOnly] = person;
console.log(nameOnly); // 输出:John

// 解构剩余元素
let numbers: [number, ...number[]] = [1, 2, 3, 4, 5];
let [first, ...rest] = numbers;
console.log(first); // 输出:1
console.log(rest); // 输出:[2, 3, 4, 5]

// 解构可选元素
let personWithTitle: [string, number, string?] = ["John", 30, "Engineer"];
let [name2, age2, title] = personWithTitle;
console.log(title); // 输出:Engineer

let personWithoutTitle: [string, number, string?] = ["John", 30];
let [name3, age3, title2] = personWithoutTitle;
console.log(title2); // 输出:undefined

展开操作符

元组可以使用展开操作符来合并元组。

typescript
let person: [string, number] = ["John", 30];
let details: [string, boolean] = ["Engineer", true];

// 合并元组
let combined: [string, number, string, boolean] = [...person, ...details];
console.log(combined); // 输出:["John", 30, "Engineer", true]

// 展开到数组
let array: string[] = [...person];
console.log(array); // 输出:["John", "30"]

元组与数组的转换

元组可以转换为数组,数组也可以转换为元组(需要类型断言)。

typescript
// 元组转换为数组
let person: [string, number] = ["John", 30];
let array: string[] = [...person];
let array2: (string | number)[] = person;

// 数组转换为元组(需要类型断言)
let array3: (string | number)[] = ["John", 30];
let person2: [string, number] = array3 as [string, number];

4. 类型守卫

在 TypeScript 中,可以使用类型守卫来检查一个值是否是元组类型。

自定义类型守卫

typescript
// 检查是否是元组
function isTuple(value: any): value is any[] {
  return Array.isArray(value) && value.length > 0;
}

// 检查是否是特定类型的元组
function isStringNumberTuple(value: any): value is [string, number] {
  return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}

function processValue(value: any) {
  if (isStringNumberTuple(value)) {
    // 在这个分支中,TypeScript 知道 value 是 [string, number] 类型
    console.log(`Name: ${value[0]}, Age: ${value[1]}`);
  } else {
    console.log(`Not a [string, number] tuple: ${value}`);
  }
}

processValue(["John", 30]); // 输出:Name: John, Age: 30
processValue(["John"]); // 输出:Not a [string, number] tuple: John
processValue([1, 2]); // 输出:Not a [string, number] tuple: 1,2

5. 元组的最佳实践

1. 使用类型注解

为元组添加类型注解可以提高代码的可读性和类型安全性。

typescript
// 推荐
let person: [string, number] = ["John", 30];

// 不推荐
let person = ["John", 30]; // 类型推断为 (string | number)[],失去了元组的类型安全

2. 使用 const 声明只读元组

对于不需要修改的元组,使用 const 声明可以防止意外修改。

typescript
// 推荐
const person: [string, number] = ["John", 30];

// 不推荐
let person: [string, number] = ["John", 30]; // 可能被意外修改

3. 合理使用可选元素

当元组的某些元素可能不存在时,使用可选元素可以提高代码的灵活性。

typescript
// 推荐:使用可选元素
let person: [string, number, string?] = ["John", 30];
let personWithTitle: [string, number, string?] = ["John", 30, "Engineer"];

// 不推荐:使用联合类型
let person: [string, number, string | undefined] = ["John", 30, undefined];

4. 合理使用剩余元素

当元组的元素数量不确定时,使用剩余元素可以提高代码的灵活性。

typescript
// 推荐:使用剩余元素
let numbers: [number, ...number[]] = [1, 2, 3, 4, 5];

// 不推荐:使用固定长度
let numbers: [number, number, number, number, number] = [1, 2, 3, 4, 5];

5. 使用解构赋值提高可读性

对于元组的元素访问,使用解构赋值可以提高代码的可读性。

typescript
// 推荐:使用解构赋值
let person: [string, number] = ["John", 30];
let [name, age] = person;
console.log(`Name: ${name}, Age: ${age}`);

// 不推荐:直接访问索引
let person: [string, number] = ["John", 30];
console.log(`Name: ${person[0]}, Age: ${person[1]}`);

6. 注意元组的长度

元组的长度是固定的,不能添加或删除元素,使用时需要注意。

typescript
// 注意:元组长度固定
let person: [string, number] = ["John", 30];

// 错误:不能添加元素
// person.push("Engineer");

// 错误:不能删除元素
// person.pop();

6. 常见错误

1. 类型不匹配

在 TypeScript 中,元组的元素类型必须与声明时的类型一致。

typescript
// 错误:类型不匹配
let person: [string, number] = ["John", "30"]; // 类型 'string' 不能赋值给类型 'number'

// 正确
let person: [string, number] = ["John", 30];

2. 长度不匹配

元组的长度是固定的,不能多也不能少。

typescript
// 错误:长度不匹配
let person: [string, number] = ["John"]; // 类型 '[string]' 不能赋值给类型 '[string, number]'

// 错误:长度不匹配
let person: [string, number] = ["John", 30, "Engineer"]; // 类型 '[string, number, string]' 不能赋值给类型 '[string, number]'

// 正确
let person: [string, number] = ["John", 30];

3. 访问越界索引

访问元组中不存在的索引会导致类型错误。

typescript
let person: [string, number] = ["John", 30];

// 错误:访问越界索引
// console.log(person[2]); // 长度为 "2" 的元组类型 '[string, number]' 在索引 "2" 处没有元素

// 正确
console.log(person[0]); // 输出:John
console.log(person[1]); // 输出:30

4. 尝试修改元组长度

元组的长度是固定的,不能通过修改 length 属性来改变长度。

typescript
let person: [string, number] = ["John", 30];

// 错误:不能修改元组长度
// person.length = 3; // 无法分配到 "length" ,因为它是只读属性

// 正确
console.log(person.length); // 输出:2

5. 混淆元组和数组

元组和数组是不同的类型,混淆它们会导致错误。

typescript
// 错误:混淆元组和数组
let person: [string, number] = ["John", 30];
let array: string[] = person; // 类型 '[string, number]' 不能赋值给类型 'string[]'

// 正确:使用类型断言
let person: [string, number] = ["John", 30];
let array: (string | number)[] = person;

7. 元组与数组的比较

1. 长度

  • 元组:长度固定,不能添加或删除元素。
  • 数组:长度可变,可以添加或删除元素。

2. 类型

  • 元组:每个元素的类型可以不同,并且类型是固定的。
  • 数组:所有元素的类型必须相同(或使用联合类型)。

3. 类型安全

  • 元组:提供更强的类型安全,访问元素时会检查类型。
  • 数组:类型安全相对较弱,访问元素时需要手动检查类型。

4. 用途

  • 元组:适用于固定结构的数据,如坐标、颜色值、键值对等。
  • 数组:适用于可变长度的同类型数据集合。

8. 实际应用场景

1. 坐标表示

typescript
// 二维坐标
let point: [number, number] = [10, 20];

// 三维坐标
let point3D: [number, number, number] = [10, 20, 30];

// 操作坐标
function distance(p1: [number, number], p2: [number, number]): number {
  return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}

let p1: [number, number] = [0, 0];
let p2: [number, number] = [3, 4];
console.log(distance(p1, p2)); // 输出:5

2. 颜色表示

typescript
// RGB 颜色
let rgb: [number, number, number] = [255, 0, 0]; // 红色

// RGBA 颜色
let rgba: [number, number, number, number] = [255, 0, 0, 1]; // 不透明红色

// 操作颜色
function toHexColor(rgb: [number, number, number]): string {
  return `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`;
}

let red: [number, number, number] = [255, 0, 0];
console.log(toHexColor(red)); // 输出:#ff0000

3. 函数返回多个值

typescript
// 返回多个值
function getUser(): [string, number, boolean] {
  return ["John", 30, true];
}

// 使用解构赋值
let [name, age, active] = getUser();
console.log(`Name: ${name}, Age: ${age}, Active: ${active}`); // 输出:Name: John, Age: 30, Active: true

4. 键值对

typescript
// 键值对元组
let keyValue: [string, any][] = [
  ["name", "John"],
  ["age", 30],
  ["active", true]
];

// 转换为对象
function toObject(pairs: [string, any][]): Record<string, any> {
  return pairs.reduce((obj, [key, value]) => {
    obj[key] = value;
    return obj;
  }, {});
}

let user = toObject(keyValue);
console.log(user); // 输出:{ name: "John", age: 30, active: true }

5. 函数参数

typescript
// 函数参数为元组
function printPerson(person: [string, number]): void {
  console.log(`Name: ${person[0]}, Age: ${person[1]}`);
}

printPerson(["John", 30]); // 输出:Name: John, Age: 30

// 使用剩余参数
function sum(first: number, ...rest: number[]): number {
  return first + rest.reduce((acc, num) => acc + num, 0);
}

sum(1, 2, 3, 4, 5); // 输出:15

总结

TypeScript 中的元组是一种特殊的数组类型,它允许存储不同类型的元素,并且长度是固定的。它包括:

  • 基本用法:声明元组,访问和修改元组元素,元组的长度
  • 类型注解:基本类型元组,复杂类型元组,可选元素,剩余元素,泛型元组
  • 元组操作:解构赋值,展开操作符,元组与数组的转换
  • 类型守卫:使用自定义类型守卫检查元组类型
  • 最佳实践:使用类型注解,使用 const 声明只读元组,合理使用可选元素和剩余元素,使用解构赋值提高可读性,注意元组的长度
  • 常见错误:类型不匹配,长度不匹配,访问越界索引,尝试修改元组长度,混淆元组和数组
  • 元组与数组的比较:长度,类型,类型安全,用途
  • 实际应用场景:坐标表示,颜色表示,函数返回多个值,键值对,函数参数

通过合理使用元组,你可以在 TypeScript 中更安全、更清晰地表示具有固定结构的数据,提高代码的可读性和类型安全性。