TypeScript 函数重载

122 阅读8分钟
graph TD
    A[函数重载] --> B[声明多个类型签名]
    A --> C[定义实现函数]
    B --> D[精确表达参数类型组合]
    C --> E[处理所有签名情况]
    A --> F[编译器类型检查]
    F --> G[调用点自动匹配签名]
    F --> H[提供准确类型推断]
    A --> I[应用场景]
    I --> J[REST API处理]
    I --> K[DOM操作]
    I --> L[数学运算]
    I --> M[输入验证器]

在 TypeScript 中,函数重载(Function Overloading)是一种强大的类型系统特性,它允许你为单一函数定义多个类型签名,从而精确描述函数在不同参数组合下的行为。这种能力使你能够创建出类型安全且接口优雅的多态函数

为什么需要函数重载?

考虑一个常见需求:实现加法函数,既能处理数字,也能处理字符串连接:

// 简单但不够精确的实现
function add(a: number | string, b: number | string): number | string {
  if (typeof a === "number" && typeof b === "number") {
    return a + b;
  } 
  return a.toString() + b.toString();
}

const result1 = add(2, 3);       // 期望是number,实际类型为 number | string
const result2 = add("TS", "4.9"); // 期望是string,实际类型为 number | string

这里的问题在于返回值类型的不确定性。函数重载解决了这个问题,提供精确的类型推断:

// 使用函数重载
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  if (typeof a === "number" && typeof b === "number") {
    return a + b;
  }
  return a.toString() + b.toString();
}

const numResult = add(2, 3);       // number
const strResult = add("TS", "4.9"); // string

函数重载的完整语法结构

函数重载包含两部分:类型签名(重载签名)实现函数

// 重载签名(类型声明)
function functionName(param1: Type1): ReturnType1;
function functionName(param1: Type2, param2: Type3): ReturnType2;
function functionName(param1: Type4, param2?: Type5): ReturnType3;

// 实现函数(包含实际逻辑)
function functionName(param1: unknown, param2?: unknown): unknown {
  // 实际函数实现
}

关键规则:

  1. 签名顺序很重要:编译器从上到下匹配签名
  2. 实现函数不可直接调用:只能通过匹配的重载签名调用
  3. 参数类型必须兼容:实现函数的参数类型必须包含所有重载签名的可能类型
  4. 返回值类型必须一致:实现函数的返回值必须是所有重载签名返回值的超集

函数重载的四种应用模式

1. 参数数量不同

function createDate(timestamp: number): Date;
function createDate(year: number, month: number, day: number): Date;
function createDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  if (month !== undefined && day !== undefined) {
    return new Date(yearOrTimestamp, month, day);
  }
  return new Date(yearOrTimestamp);
}

const d1 = createDate(1609459200000);     // 使用时间戳
const d2 = createDate(2023, 0, 1);        // 使用年月日

2. 参数类型不同

interface Circle { kind: "circle"; radius: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }

function calculateArea(shape: Circle): number;
function calculateArea(shape: Rectangle): number;
function calculateArea(shape: Circle | Rectangle): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius * shape.radius;
    case "rectangle":
      return shape.width * shape.height;
  }
}

const circleArea = calculateArea({ kind: "circle", radius: 10 });     // 精确返回number
const rectArea = calculateArea({ kind: "rectangle", width: 5, height: 10 }); 

3. 参数数量与类型组合不同

// 重载1: 字符串搜索,返回索引
function search(source: string, substring: string): number;
// 重载2: 数组搜索,返回元素或undefined
function search<T>(array: T[], predicate: (item: T) => boolean): T | undefined;
// 实现函数
function search(source: any, criteria: any): any {
  if (typeof source === "string") {
    return source.indexOf(criteria);
  }
  return source.find(criteria);
}

const index = search("TypeScript", "Script");  // 返回number
const item = search([1, 2, 3], (n) => n > 2); // 返回number | undefined

4. 可选参数与默认值

type Matrix = number[][];

// 重载1: 提供单个值填充整个矩阵
function createMatrix(size: number, defaultValue?: number): Matrix;
// 重载2: 提供值生成函数
function createMatrix(size: number, initFn: (row: number, col: number) => number): Matrix;
// 实现函数
function createMatrix(size: number, init: any = 0): Matrix {
  const matrix: Matrix = [];
  
  // 处理函数生成器
  if (typeof init === "function") {
    for (let i = 0; i < size; i++) {
      matrix[i] = [];
      for (let j = 0; j < size; j++) {
        matrix[i][j] = init(i, j);
      }
    }
  } else {
    // 处理默认值
    for (let i = 0; i < size; i++) {
      matrix[i] = Array(size).fill(init);
    }
  }
  
  return matrix;
}

// 3x3 矩阵,元素全为0
const zeros = createMatrix(3); 
// 3x3 矩阵,元素全为1
const ones = createMatrix(3, 1);
// 3x3 对角线为1,其余为0
const identity = createMatrix(3, (row, col) => row === col ? 1 : 0);

实际应用场景

1. REST API 请求封装

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

// 无请求体的情况
function request(url: string, method: "GET"): Promise<Response>;
// 包含请求体的情况
function request(url: string, method: Exclude<HttpMethod, "GET">, body: object): Promise<Response>;
// 实现函数
async function request(url: string, method: HttpMethod, body?: object): Promise<Response> {
  const options: RequestInit = { method };
  
  if (body) {
    options.headers = { "Content-Type": "application/json" };
    options.body = JSON.stringify(body);
  }
  
  return fetch(url, options);
}

// 使用示例
const getResponse = await request("/api/users", "GET"); // GET无需body
const postResponse = await request("/api/users", "POST", { name: "Alice" });
const putResponse = await request("/api/users/1", "PUT", { name: "Bob" });
// 错误尝试:给GET提供body
const invalid = await request("/api", "GET", {}); // 报错: Unexpected parameter

2. DOM操作辅助函数

// 重载1: 创建新元素
function dom(tag: string, attributes?: object): HTMLElement;
// 重载2: 选择现有元素
function dom(selector: string, parent?: Element): Element | null;
// 实现函数
function dom(param1: string, param2?: any): Element | HTMLElement | null {
  if (param1.startsWith("<") && param1.endsWith(">")) {
    // 创建元素模式
    const tag = param1.slice(1, -1);
    const element = document.createElement(tag);
    
    if (param2) {
      Object.entries(param2).forEach(([key, value]) => {
        element.setAttribute(key, value as string);
      });
    }
    return element;
  } else {
    // 查询元素模式
    const parent = param2 || document;
    return parent.querySelector(param1);
  }
}

// 创建元素
const button = dom("<button>", { id: "submit", class: "btn" });
// 查询元素
const form = dom("#loginForm");

3. 数学函数库

// 计算两点距离
function distance(p1: number, p2: number): number; // 坐标轴上的点
function distance(p1: [number, number], p2: [number, number]): number; // 二维点
function distance(p1: [number, number, number], p2: [number, number, number]): number; // 三维点
function distance(p1: any, p2: any): number {
  if (typeof p1 === "number" && typeof p2 === "number") {
    return Math.abs(p1 - p2);
  }
  
  if (Array.isArray(p1) && Array.isArray(p2)) {
    if (p1.length === 2 && p2.length === 2) {
      // 二维距离
      const [x1, y1] = p1;
      const [x2, y2] = p2;
      return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
    }
    if (p1.length === 3 && p2.length === 3) {
      // 三维距离
      const [x1, y1, z1] = p1;
      const [x2, y2, z2] = p2;
      return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2);
    }
  }
  
  throw new Error("Invalid argument types");
}

// 使用示例
const axisDistance = distance(5, 10); // 5
const twoDDistance = distance([0, 0], [3, 4]); // 5
const threeDDistance = distance([1, 2, 3], [4, 6, 9]); // sqrt(50) ≈ 7.07

函数重载的陷阱与解决方案

1. 签名顺序错误

问题实例:

function example(x: any): number;
function example(x: string): string;
function example(x: any): any {
  return x;
}

const result = example("hello"); // 返回类型为number,但应该是string

解决方案: 将更具体的签名放在前面

function example(x: string): string;
function example(x: any): number;
function example(x: any): any {
  return x;
}

2. 忽略可选参数和剩余参数

问题实例:

function log(message: string, userId?: number): void;
function log(message: string): void {
  console.log(message, userId || "Anonymous");
}

log("User logged in", 123); // 错误: 参数数量不匹配

解决方案: 在实现中正确处理可选参数

function log(message: string, userId?: number): void;
function log(message: string, userId?: number): void {
  console.log(message, userId || "Anonymous");
}

3. 无法区分参数类型

问题实例:

function merge(
  a: string | number,
  b: string | number
): string | number {
  if (typeof a === "string" && typeof b === "string") {
    return a + b;
  }
  if (typeof a === "number" && typeof b === "number") {
    return a + b;
  }
  throw new Error("Invalid types");
}

解决方案: 使用函数重载提供精确类型

function merge(a: string, b: string): string;
function merge(a: number, b: number): number;
function merge(
  a: string | number,
  b: string | number
): string | number {
  // 实现同上 (但调用时类型安全)
}

进阶技巧:结合泛型与重载

将函数重载与泛型结合,创建强大且类型安全的API:

类型安全的事件处理系统

// 事件映射类型
type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; code: number };
  error: Error;
};

// 重载签名
function trigger<T extends keyof EventMap>(
  eventName: T, 
  eventData: EventMap[T]
): void;

function trigger(eventName: string, eventData?: unknown): void;

// 实现函数
function trigger(eventName: any, eventData?: any): void {
  console.log(`Triggering ${eventName}`, eventData);
  // 实际触发事件逻辑...
}

// 类型安全的使用
trigger("click", { x: 10, y: 20 });   // 正确
trigger("keydown", { key: "Enter", code: 13 }); // 正确
trigger("click", { key: "A" });      // 错误: 缺少x,y属性
trigger("scroll", { delta: 100 });   // 使用字符串重载 - 允许未知事件

链式方法重载

适用于构建流畅API接口:

class QueryBuilder {
  // 重载1: 设置整数值
  where(key: string, value: number): this;
  // 重载2: 设置布尔值
  where(key: string, value: boolean): this;
  // 重载3: 设置字符串值
  where(key: string, value: string): this;
  // 重载4: 设置范围条件
  where(key: string, min: number, max: number): this;

  // 实现函数
  where(key: string, valueOrMin: any, max?: number): this {
    if (max !== undefined) {
      console.log(`Adding range condition: ${key} BETWEEN ${valueOrMin} AND ${max}`);
    } else {
      console.log(`Adding condition: ${key} = ${valueOrMin}`);
    }
    return this; // 返回this支持链式调用
  }
}

const query = new QueryBuilder()
  .where("age", 25)         // 精确值
  .where("active", true)     // 布尔值
  .where("name", "Alice")    // 字符串
  .where("score", 80, 100);  // 范围

函数重载最佳实践

  1. 保持简单:避免超过4个重载签名,复杂的可考虑重构
  2. 精确命名:使用表达性强的函数名反映不同重载的行为
  3. 文档清晰:为每个重载签名添加JSDoc注释
  4. 一致性优先:保持不同重载的返回类型相似
  5. 测试覆盖:为每个重载路径编写单元测试
/**
 * 处理不同类型的输入,返回格式化信息
 * 
 * @overload 处理数字输入
 * @param value - 数值
 * @returns 格式化货币字符串
 */
function process(value: number): string;

/**
 * @overload 处理日期输入
 * @param value - 日期对象
 * @returns 格式化日期字符串
 */
function process(value: Date): string;

/**
 * @overload 处理自定义对象
 * @param value - 用户对象
 * @returns 格式化用户信息
 */
function process(value: { name: string; age: number }): string;

function process(value: any): string {
  // 统一实现
}

何时避免函数重载?

在某些场景下,其他类型技术可能更合适:

  1. 参数类型组合过多 → 使用 对象参数 替代

    // 不推荐:多个重载
    function draw(size: number): void;
    function draw(width: number, height: number): void;
    
    // 推荐:单一签名,对象参数
    function draw(options: 
      | { size: number } 
      | { width: number; height: number }
    ): void;
    
  2. 返回值类型复杂多变 → 使用 条件类型

    // 使用条件类型替代重载
    async function fetchData<T>(id: T): Promise<
      T extends number ? User : 
      T extends string ? Product : 
      never
    >;
    
  3. 相似参数不同类型 → 使用 泛型

    // 使用泛型替代多个重载
    function identity<T>(arg: T): T {
      return arg;
    }
    

函数重载的三大价值

  1. 提升类型安全性
    在编译时确保函数调用的参数和返回值类型正确,避免运行时错误

  2. 改善开发体验
    IDE和编辑器能提供精确的自动补全和文档提示,提高效率

  3. 增强代码表达能力
    清晰地表达函数支持的不同用法模式,自文档化代码