强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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, "[email protected]"];

// 函数参数使用标签元组
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);
  }
}

注意事项

  1. 空数组需要类型注解——否则推断为 any[]
  2. 元组有固定长度和类型——每个位置的类型是确定的
  3. 只读数组不能调用修改方法——pushpopsplice
  4. filter 不会自动收窄类型——需要类型守卫或类型谓词
  5. 剩余元素只能在元组末尾——[string, ...number[]] 是合法的

扩展阅读