数组和元组
概述
数组(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 设计中非常有用
相关链接
- 基础类型 - 了解 TypeScript 的基础类型
- 对象类型 - 学习对象类型定义
- 字面量类型 - 了解字面量类型
- 联合类型 - 学习联合类型的使用
- TypeScript 官方文档 - 数组
- TypeScript 官方文档 - 元组