Skip to content

数组和元组

概述

数组(Array)和元组(Tuple)是 TypeScript 中用于表示有序集合数据的两种类型。数组类型用于表示相同类型元素的集合,而元组类型用于表示固定长度和固定类型顺序的元素集合。理解数组和元组类型对于处理列表数据、函数参数和返回值非常重要。

数组类型

基本语法

TypeScript 提供了两种方式定义数组类型:

typescript
// 方式 1:使用类型 + 方括号(推荐)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// 方式 2:使用 Array 泛型
let numbers2: Array<number> = [1, 2, 3, 4, 5];
let names2: Array<string> = ["Alice", "Bob", "Charlie"];

两种方式功能相同,但第一种方式更简洁,也更常用。

数组操作

数组类型支持所有 JavaScript 数组方法,TypeScript 会根据元素类型提供类型检查:

typescript
// 定义数字数组
let numbers: number[] = [1, 2, 3, 4, 5];

// 数组方法都有类型支持
numbers.push(6);              // ✅ 正确:添加数字
numbers.push("hello");        // ❌ 错误:不能添加字符串
let first = numbers[0];       // 类型推断为 number
let length = numbers.length;  // 类型推断为 number

// 数组方法
let doubled = numbers.map(n => n * 2);        // 返回 number[]
let filtered = numbers.filter(n => n > 3);    // 返回 number[]
let sum = numbers.reduce((a, b) => a + b, 0); // 返回 number

多维数组

数组类型可以嵌套,用于表示多维数组:

typescript
// 二维数组:数组的数组
let matrix: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// 三维数组
let cube: number[][][] = [
  [[1, 2], [3, 4]],
  [[5, 6], [7, 8]]
];

// 使用 Array 泛型语法
let matrix2: Array<Array<number>> = [[1, 2], [3, 4]];

只读数组

使用 readonly 关键字可以创建只读数组,防止数组被修改:

typescript
// 只读数组:使用 readonly 关键字
let readonlyNumbers: readonly number[] = [1, 2, 3, 4, 5];

// ✅ 正确:可以读取
let first = readonlyNumbers[0];
let length = readonlyNumbers.length;

// ❌ 错误:不能修改
readonlyNumbers.push(6);        // Error: Property 'push' does not exist on type 'readonly number[]'
readonlyNumbers[0] = 10;        // Error: Index signature in type 'readonly number[]' only permits reading
readonlyNumbers.pop();          // Error: Property 'pop' does not exist on type 'readonly number[]'

// 使用 ReadonlyArray 类型(另一种方式)
let readonlyNumbers2: ReadonlyArray<number> = [1, 2, 3];

提示

readonly number[]ReadonlyArray<number> 功能相同,但 readonly number[] 语法更简洁,推荐使用。

联合类型数组

数组元素可以是联合类型,表示数组可以包含多种类型的元素:

typescript
// 联合类型数组:可以包含 string 或 number
let mixed: (string | number)[] = ["hello", 42, "world", 100];

// 访问元素时,类型是联合类型
let first = mixed[0];  // 类型:string | number

// 需要类型守卫来缩小类型
if (typeof first === "string") {
  console.log(first.toUpperCase());  // 此时 TypeScript 知道 first 是 string
} else {
  console.log(first.toFixed(2));     // 此时 TypeScript 知道 first 是 number
}

元组类型

基本语法

元组(Tuple)类型表示一个已知元素数量和类型的数组,各元素的类型不必相同:

typescript
// 定义元组类型:固定长度和类型
let tuple: [string, number] = ["hello", 42];

// 访问元素时,类型是精确的
let first: string = tuple[0];   // 类型:string
let second: number = tuple[1];  // 类型:number

// ❌ 错误:长度不匹配
let tuple2: [string, number] = ["hello"];  // Error: Type '[string]' is not assignable to type '[string, number]'

// ❌ 错误:类型不匹配
let tuple3: [string, number] = [42, "hello"];  // Error: Type 'number' is not assignable to type 'string'

元组的使用场景

元组常用于以下场景:

typescript
// 场景 1:函数返回多个值
function getUserInfo(): [string, number, boolean] {
  return ["Alice", 30, true];
}

const [name, age, isActive] = getUserInfo();
console.log(name);      // "Alice" (string)
console.log(age);       // 30 (number)
console.log(isActive);  // true (boolean)

// 场景 2:表示坐标点
type Point = [number, number];
let point: Point = [10, 20];
let [x, y] = point;

// 场景 3:表示键值对
type KeyValue = [string, any];
let config: KeyValue[] = [
  ["theme", "dark"],
  ["language", "zh-CN"],
  ["version", 1.0]
];

可选元组元素

TypeScript 3.0+ 支持可选元组元素,使用 ? 标记:

typescript
// 可选元组元素:第三个元素是可选的
type OptionalTuple = [string, number, boolean?];

let tuple1: OptionalTuple = ["hello", 42];           // ✅ 正确:第三个元素可选
let tuple2: OptionalTuple = ["hello", 42, true];    // ✅ 正确:提供第三个元素
let tuple3: OptionalTuple = ["hello"];               // ❌ 错误:第二个元素是必需的

// 访问可选元素时需要检查
let tuple: OptionalTuple = ["hello", 42];
if (tuple[2] !== undefined) {
  console.log(tuple[2]);  // 类型:boolean
}

剩余元组元素

TypeScript 4.0+ 支持剩余元组元素,使用扩展运算符:

typescript
// 剩余元组元素:前两个固定,后面可以有任意数量的 string
type RestTuple = [number, string, ...string[]];

let tuple1: RestTuple = [1, "hello"];
let tuple2: RestTuple = [1, "hello", "world"];
let tuple3: RestTuple = [1, "hello", "world", "foo", "bar"];

// 访问剩余元素
let tuple: RestTuple = [1, "hello", "world", "foo"];
let first = tuple[0];    // number
let second = tuple[1];    // string
let rest = tuple.slice(2);  // string[]

只读元组

使用 readonly 关键字可以创建只读元组:

typescript
// 只读元组
let readonlyTuple: readonly [string, number] = ["hello", 42];

// ✅ 正确:可以读取
let first = readonlyTuple[0];
let second = readonlyTuple[1];

// ❌ 错误:不能修改
readonlyTuple[0] = "world";  // Error: Cannot assign to '0' because it is a read-only property
readonlyTuple.push("foo");   // Error: Property 'push' does not exist on type 'readonly [string, number]'

标记元组

TypeScript 4.0+ 支持标记元组(Labeled Tuples),为元组元素添加标签以提高可读性:

typescript
// 标记元组:为每个元素添加标签
type LabeledTuple = [name: string, age: number, active: boolean];

let user: LabeledTuple = ["Alice", 30, true];

// 标签不影响类型检查,但提高代码可读性
function createUser(): LabeledTuple {
  return ["Bob", 25, false];
}

// 解构时可以使用标签作为注释
const [name, age, active] = createUser();
// name: string, age: number, active: boolean

信息

标记元组的标签仅用于提高代码可读性,不影响类型检查。在解构时,标签不会作为变量名使用。

数组与元组的区别

特性数组(Array)元组(Tuple)
长度可变固定(或可变剩余元素)
元素类型通常相同可以不同
使用场景同类型数据集合固定结构的数据
类型检查检查元素类型检查元素类型和位置
示例number[][string, number]

使用示例

示例 1:处理用户列表

typescript
// 定义用户类型
type User = {
  id: number;
  name: string;
  email: string;
};

// 用户数组
let users: User[] = [
  { id: 1, name: "Alice", email: "alice@example.com" },
  { id: 2, name: "Bob", email: "bob@example.com" },
  { id: 3, name: "Charlie", email: "charlie@example.com" }
];

// 数组操作
function findUserById(users: User[], id: number): User | undefined {
  return users.find(user => user.id === id);
}

function filterUsersByEmail(users: User[], domain: string): User[] {
  return users.filter(user => user.email.endsWith(domain));
}

// 使用示例
let user = findUserById(users, 2);
if (user) {
  console.log(user.name);  // "Bob"
}

let gmailUsers = filterUsersByEmail(users, "@gmail.com");
console.log(gmailUsers.length);

示例 2:API 响应处理

typescript
// 使用元组表示 API 响应:[状态码, 数据, 错误信息]
type ApiResponse<T> = [number, T | null, string | null];

function fetchUser(id: number): ApiResponse<User> {
  // 模拟 API 调用
  if (id > 0) {
    return [200, { id, name: "Alice", email: "alice@example.com" }, null];
  } else {
    return [404, null, "User not found"];
  }
}

// 使用示例
const [statusCode, data, error] = fetchUser(1);

if (statusCode === 200 && data) {
  console.log("用户:", data.name);
} else if (error) {
  console.error("错误:", error);
}

示例 3:配置管理

typescript
// 使用元组表示配置项:[键, 值, 是否必需]
type ConfigItem = [string, string | number | boolean, boolean];

// 配置数组
let config: ConfigItem[] = [
  ["apiUrl", "https://api.example.com", true],
  ["timeout", 5000, false],
  ["retries", 3, false],
  ["debug", false, false]
];

// 配置处理函数
function getConfigValue(config: ConfigItem[], key: string): ConfigItem[1] | undefined {
  const item = config.find(([k]) => k === key);
  return item ? item[1] : undefined;
}

function validateConfig(config: ConfigItem[]): boolean {
  return config.every(([, , required]) => {
    if (required) {
      // 检查必需配置是否存在
      return true;  // 简化示例
    }
    return true;
  });
}

// 使用示例
let apiUrl = getConfigValue(config, "apiUrl");
console.log(apiUrl);  // "https://api.example.com"

示例 4:数据转换

typescript
// 使用数组方法进行数据转换
let numbers: number[] = [1, 2, 3, 4, 5];

// Map:转换每个元素
let doubled = numbers.map(n => n * 2);  // [2, 4, 6, 8, 10]

// Filter:过滤元素
let evens = numbers.filter(n => n % 2 === 0);  // [2, 4]

// Reduce:累积计算
let sum = numbers.reduce((acc, n) => acc + n, 0);  // 15

// 链式调用
let result = numbers
  .filter(n => n > 2)      // [3, 4, 5]
  .map(n => n * n)          // [9, 16, 25]
  .reduce((a, b) => a + b, 0);  // 50

console.log(result);

类型检查示例

常见错误

typescript
// ❌ 错误:数组类型不匹配
let numbers: number[] = [1, 2, 3];
numbers.push("hello");  // Error: Argument of type 'string' is not assignable to parameter of type 'number'

// ❌ 错误:元组长度不匹配
let tuple: [string, number] = ["hello"];  // Error: Type '[string]' is not assignable to type '[string, number]'

// ❌ 错误:元组类型顺序错误
let tuple2: [string, number] = [42, "hello"];  // Error: Type 'number' is not assignable to type 'string'

// ❌ 错误:修改只读数组
let readonlyArray: readonly number[] = [1, 2, 3];
readonlyArray.push(4);  // Error: Property 'push' does not exist on type 'readonly number[]'

// ❌ 错误:修改只读元组
let readonlyTuple: readonly [string, number] = ["hello", 42];
readonlyTuple[0] = "world";  // Error: Cannot assign to '0' because it is a read-only property

正确写法

typescript
// ✅ 正确:数组类型匹配
let numbers: number[] = [1, 2, 3];
numbers.push(4);

// ✅ 正确:元组长度和类型匹配
let tuple: [string, number] = ["hello", 42];

// ✅ 正确:使用联合类型数组
let mixed: (string | number)[] = ["hello", 42, "world"];

// ✅ 正确:只读数组可以读取
let readonlyArray: readonly number[] = [1, 2, 3];
let first = readonlyArray[0];  // 可以读取

// ✅ 正确:使用类型守卫处理联合类型
let mixedArray: (string | number)[] = ["hello", 42];
mixedArray.forEach(item => {
  if (typeof item === "string") {
    console.log(item.toUpperCase());
  } else {
    console.log(item.toFixed(2));
  }
});

注意事项

提示

  • 数组类型使用 number[] 语法比 Array<number> 更简洁,推荐使用
  • 元组类型适合表示固定结构的数据,如函数返回多个值、坐标点等
  • 使用只读数组和元组可以防止意外修改,提高代码安全性
  • 标记元组虽然不影响类型检查,但可以大大提高代码可读性

注意

  • 元组类型在访问超出索引的元素时,TypeScript 会推断为所有元素类型的联合类型
  • 只读数组和元组只在编译时生效,运行时仍然可以修改(需要使用 Object.freeze() 实现真正的不可变)
  • 使用联合类型数组时,需要类型守卫来缩小类型范围

重要

  • 数组和元组是 TypeScript 中处理集合数据的基础类型
  • 理解数组和元组的区别,选择合适的数据结构可以提高代码的类型安全性
  • 元组类型在函数式编程和 API 设计中非常有用

相关链接

基于 VitePress 构建