TypeScript 开发指南 / 15 - 数组与元组
数组与元组
数组类型
基本数组类型
// 方式一:类型[]
const numbers: number[] = [1, 2, 3];
const names: string[] = ["Alice", "Bob", "Charlie"];
const flags: boolean[] = [true, false, true];
// 方式二:Array<类型>(泛型语法)
const numbers2: Array<number> = [1, 2, 3];
const names2: Array<string> = ["Alice", "Bob"];
// 两种方式等价,推荐使用 类型[] 语法(更简洁)
复杂数组类型
// 联合类型数组
const mixed: (string | number)[] = [1, "two", 3, "four"];
// 对象数组
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
// 二维数组
const matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 多维数组
const cube: number[][][] = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]];
数组类型推断
// TypeScript 自动推断数组类型
const nums = [1, 2, 3]; // number[]
const strs = ["a", "b", "c"]; // string[]
const mixed = [1, "a", true]; // (string | number | boolean)[]
// 空数组需要注解
let items = []; // any[]
let items2: string[] = []; // string[]
// as const 推断为只读元组
const tuple = [1, "hello"] as const; // readonly [1, "hello"]
数组方法的类型
const numbers: number[] = [1, 2, 3, 4, 5];
// map:返回新数组
const doubled = numbers.map(n => n * 2); // number[]
const strings = numbers.map(n => n.toString()); // string[]
// filter:类型收窄
const items: (string | number)[] = [1, "two", 3, "four"];
const stringsOnly = items.filter(
(item): item is string => typeof item === "string"
); // string[]
// reduce:类型推断
const sum = numbers.reduce((acc, n) => acc + n, 0); // number
// find:返回 T | undefined
const found = numbers.find(n => n > 3); // number | undefined
// some / every:返回 boolean
const hasNegative = numbers.some(n => n < 0); // boolean
const allPositive = numbers.every(n => n > 0); // boolean
// includes:类型检查
const includes = numbers.includes(3); // boolean
泛型数组方法
// flat:展平嵌套数组
const nested = [[1, 2], [3, [4, 5]]];
const flat1 = nested.flat(); // (number | number[])[]
const flat2 = nested.flat(2); // number[]
// flatMap:map + flat
const sentences = ["Hello World", "TypeScript is great"];
const words = sentences.flatMap(s => s.split(" ")); // string[]
// concat:连接数组
const a = [1, 2];
const b = [3, 4];
const c = a.concat(b); // number[]
// slice:切片
const slice = numbers.slice(1, 3); // number[]
// splice:修改数组(会改变原数组)
const removed = numbers.splice(1, 2); // number[]
元组类型(Tuple Types)
元组是固定长度、每个位置有固定类型的数组:
// 基本元组
const pair: [string, number] = ["Alice", 25];
// 访问元素
const name = pair[0]; // string
const age = pair[1]; // number
// ❌ 错误:类型不匹配
const wrong: [string, number] = [25, "Alice"];
// ❌ 错误:长度不匹配
const wrong2: [string, number] = ["Alice", 25, true];
元组解构
const user: [string, number, boolean] = ["Alice", 25, true];
// 解构
const [name, age, active] = user;
// name: string, age: number, active: boolean
// 函数返回元组
function useState<T>(initial: T): [T, (value: T) => void] {
let state = initial;
return [
state,
(value: T) => { state = value; }
];
}
const [count, setCount] = useState(0);
可选元素
// 可选元组元素
type Config = [string, number, boolean?];
const config1: Config = ["localhost", 3000]; // ✅
const config2: Config = ["localhost", 3000, true]; // ✅
// 可选元素只能在末尾
type MaybeError = [string, Error?];
剩余元素(Rest Elements)
// 剩余元素
type StringNumberPairs = [string, ...number[]];
const pair1: StringNumberPairs = ["hello"];
const pair2: StringNumberPairs = ["hello", 1, 2, 3];
// 混合
type Mixed = [string, number, ...boolean[]];
const mixed: Mixed = ["hello", 1, true, false, true];
// 开头 rest
type LeadingRest = [...string[], number];
const leading: LeadingRest = ["a", "b", 1];
带标签的元组(Labeled Tuples)
// 标签元组(TypeScript 4.0+)
type UserTuple = [name: string, age: number, email: string];
const user: UserTuple = ["Alice", 25, "alice@example.com"];
// 函数参数使用标签元组
function createUser(...args: UserTuple): User {
const [name, age, email] = args;
return { id: 0, name, age, email, password: "" };
}
// 更好的可读性
type HttpResponse = [
statusCode: number,
headers: Record<string, string>,
body: string
];
// 函数类型
type CreateTuple = (...args: UserTuple) => User;
只读数组和元组
readonly 数组
// readonly 修饰符
const readonlyArr: readonly number[] = [1, 2, 3];
// ❌ 不能修改
readonlyArr.push(4); // Error
readonlyArr[0] = 10; // Error
readonlyArr.splice(0, 1); // Error
// ✅ 可以读取
console.log(readonlyArr[0]); // 1
console.log(readonlyArr.length); // 3
console.log(readonlyArr.map(n => n * 2)); // [2, 4, 6]
// ReadonlyArray 泛型
const readonlyArr2: ReadonlyArray<number> = [1, 2, 3];
Readonly 元组
// 只读元组
const readonlyTuple: readonly [string, number] = ["Alice", 25];
// ❌ 不能修改
readonlyTuple[0] = "Bob"; // Error
// Readonly<T> 工具类型
type MutableTuple = [string, number];
type ReadonlyTuple = Readonly<MutableTuple>;
数组与泛型
类型安全的数组操作
// 泛型数组函数
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
function chunk<T>(arr: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
// 使用
const nums = [1, 2, 2, 3, 3, 4];
const uniqueNums = unique(nums); // number[]
const chunks = chunk(nums, 2); // number[][]
类型安全的过滤
// 类型守卫过滤
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const items: (string | null | undefined)[] = ["a", null, "b", undefined, "c"];
const defined = items.filter(isDefined); // string[]
// 类型谓词过滤
function isString(value: unknown): value is string {
return typeof value === "string";
}
const mixed: (string | number)[] = [1, "two", 3, "four"];
const strings = mixed.filter(isString); // string[]
数组与条件类型
// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<(string | number)[]>; // string | number
// 判断是否为数组
type IsArray<T> = T extends any[] ? true : false;
type D = IsArray<string[]>; // true
type E = IsArray<string>; // false
// 数组转联合类型
type ArrayToUnion<T extends any[]> = T[number];
type F = ArrayToUnion<[string, number, boolean]>; // string | number | boolean
业务场景:分页数据
// 分页响应类型
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}
// 分页工具
class Paginator<T> {
private currentPage: number = 1;
private allItems: T[] = [];
constructor(
private fetcher: (page: number, pageSize: number) => Promise<PaginatedResponse<T>>,
private pageSize: number = 10
) {}
async loadPage(page: number): Promise<T[]> {
const response = await this.fetcher(page, this.pageSize);
this.currentPage = page;
return response.data;
}
async loadAll(): Promise<T[]> {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await this.fetcher(page, this.pageSize);
this.allItems.push(...response.data);
hasMore = response.pagination.hasNext;
page++;
}
return this.allItems;
}
// 获取当前页的数据切片
getPage(items: T[], page: number): T[] {
const start = (page - 1) * this.pageSize;
return items.slice(start, start + this.pageSize);
}
}
注意事项
- 空数组需要类型注解——否则推断为
any[] - 元组有固定长度和类型——每个位置的类型是确定的
- 只读数组不能调用修改方法——
push、pop、splice等 filter不会自动收窄类型——需要类型守卫或类型谓词- 剩余元素只能在元组末尾——
[string, ...number[]]是合法的