第 5 章

函数类型:重载、泛型函数、call signature

第5章:函数类型:重载、泛型函数、call signature

TypeScript 中函数类型的完整表达能力:重载、泛型、this 参数、void 语义各有什么规则?这是理解本章内容的出发点。

本章核心问题:TypeScript 中函数类型的完整表达能力:重载、泛型、this 参数、void 语义各有什么规则?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

函数类型的两种语法

TypeScript 有两种方式描述函数的类型,表达的是同一件事,但用途不同:

// 方式 1:箭头语法(最常用,适合类型别名)
type Add = (a: number, b: number) => number;

// 方式 2:call signature 对象语法(适合描述有属性的函数)
type Add2 = {
  (a: number, b: number): number;
};

// 使用时完全一致
const add: Add = (a, b) => a + b;
const add2: Add2 = (a, b) => a + b;

区别在于:对象语法可以在同一个类型里同时声明属性和 call signature,这是箭头语法做不到的(见后文的 callable 对象部分)。

参数修饰符:可选、默认值、剩余参数

// 可选参数:用 ? 标记,类型自动变为 T | undefined
function greet(name: string, greeting?: string): string {
  return `${greeting ?? "Hello"}, ${name}!`;
}

greet("Alice");           // "Hello, Alice!"
greet("Alice", "Hi");    // "Hi, Alice!"

// 默认值:函数签名中已处理 undefined,调用方不感知
function createUser(name: string, role: string = "user") {
  return { name, role };
}

createUser("Alice");              // { name: "Alice", role: "user" }
createUser("Bob", "admin");       // { name: "Bob", role: "admin" }

// 剩余参数:类型是数组
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

sum(1, 2, 3, 4);  // 10

// 剩余参数也可以是元组类型(TypeScript 4.0+)
function log(message: string, ...tags: [string, ...string[]]): void {
  console.log(`[${tags.join("][")}] ${message}`);
}

log("Server started", "info", "startup");

可选参数和默认值的区别:

// 可选参数:调用方传 undefined 是合法的
function withOptional(x?: number) {
  // x 的类型是 number | undefined
  return x ?? 0;
}

// 默认值参数:传 undefined 会触发默认值
function withDefault(x: number = 0) {
  // x 的类型是 number(已处理 undefined)
  return x;
}

withOptional(undefined);  // OK,返回 0
withDefault(undefined);   // OK,返回 0(触发默认值)

函数重载:一个函数名,多种调用方式

函数重载让同一个函数根据参数类型或数量返回不同类型。重载签名(overload signatures)描述所有合法的调用方式,实现签名(implementation signature)是实际代码,但不对外可见。

// 重载签名(向外暴露)
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
// 实现签名(不可从外部调用,必须兼容所有重载)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

// 调用时 TypeScript 根据参数推断返回类型
const div   = createElement("div");    // 类型:HTMLDivElement
const span  = createElement("span");   // 类型:HTMLSpanElement
const input = createElement("input");  // 类型:HTMLInputElement
// createElement("section");           // 错误:没有匹配的重载签名

实现签名不可调用——这是最常见的误解:

// 重载签名
function format(value: string): string;
function format(value: number): string;
// 实现签名(处理所有情况,但不能直接用这个签名调用)
function format(value: string | number): string {
  if (typeof value === "string") {
    return value.trim();
  }
  return value.toFixed(2);
}

// 外部只能用重载签名调用
format("hello");  // OK
format(3.14);     // OK
// format(true);  // 错误:boolean 不匹配任何重载

根据参数数量重载:

// 0个或1个参数,返回类型不同
function getDate(): Date;
function getDate(timestamp: number): Date;
function getDate(timestamp?: number): Date {
  return timestamp !== undefined ? new Date(timestamp) : new Date();
}

const now   = getDate();            // Date
const fixed = getDate(1700000000000); // Date

反模式:不该用重载时用了重载

很多情况下,联合类型比重载更简洁,也更易维护:

// 反模式:用重载处理联合参数,返回类型不变
function print(value: string): void;
function print(value: number): void;
function print(value: string | number): void {
  console.log(String(value));
}

// 正确:直接用联合类型
function print(value: string | number): void {
  console.log(String(value));
}

需要重载的条件:不同参数组合对应不同返回类型,且联合类型无法准确表达这种对应关系。

// 这里用联合类型表达不清:当 asArray=true 时返回 string[],否则返回 string
// 普通联合类型做不到这一点
function parseCSV(data: string, asArray: true): string[];
function parseCSV(data: string, asArray?: false): string;
function parseCSV(data: string, asArray?: boolean): string | string[] {
  const result = data.split(",").map(s => s.trim());
  return asArray ? result : result.join(", ");
}

const arr: string[] = parseCSV("a, b, c", true);
const str: string   = parseCSV("a, b, c");

泛型函数:<T> 让函数适用于任意类型

当函数的逻辑和具体类型无关时,用泛型代替 any。泛型保留类型信息,any 丢弃类型信息。

// 不好:用 any 丢失了类型
function first(arr: any[]): any {
  return arr[0];
}

const x = first([1, 2, 3]);  // x 的类型是 any,失去了类型保护

// 好:用泛型保留类型
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = first([1, 2, 3]);        // n 的类型是 number | undefined
const s = first(["a", "b"]);       // s 的类型是 string | undefined
const u = first([]);               // u 的类型是 undefined

多个类型参数:

function zip<A, B>(as: A[], bs: B[]): [A, B][] {
  return as.map((a, i) => [a, bs[i]] as [A, B]);
}

const pairs = zip([1, 2, 3], ["a", "b", "c"]);
// pairs 类型:[number, string][]
// pairs = [[1, "a"], [2, "b"], [3, "c"]]

用约束限制泛型的范围:

// T 必须有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");          // "hello" — 类型 string
longest([1, 2, 3], [1, 2]);      // [1, 2, 3] — 类型 number[]
// longest(1, 2);                 // 错误:number 没有 length 属性

// 约束键名必须是对象的属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name");    // 类型:string
const id   = getProperty(user, "id");      // 类型:number
// getProperty(user, "email");             // 错误:email 不是 user 的属性

Level 2 · 它是怎么运行的(3-5年经验)

this 参数的类型

普通函数(非箭头函数)中 this 的类型可以在参数列表第一位显式声明。这个参数是假参数,编译后不存在:

interface Button {
  label: string;
  onClick(this: Button): void;
}

const btn: Button = {
  label: "Click me",
  onClick(this: Button) {
    console.log(this.label);  // TypeScript 知道 this 是 Button
  },
};

btn.onClick();  // OK

// 防止在错误的上下文中调用
const handler = btn.onClick;
handler();  // 错误:类型为 void 的 this 上下文不能分配给 Button 类型的 this

// 箭头函数捕获外层 this,不需要这个声明
class Counter {
  count = 0;

  increment = (): void => {
    this.count++;  // 箭头函数,this 是 Counter 实例
  };
}

void vs undefined:微妙的差别

voidundefined 看起来相似,但含义不同:

// void:调用方不应使用返回值,但函数可以 return 任何值(甚至 undefined 以外的)
type VoidFn = () => void;

// 这是合法的!void 只是说"忽略返回值"
const arr = [1, 2, 3];
const result: number[] = [];

// Array.prototype.forEach 的回调类型是 () => void
// 所以 push 返回 number 不会报错
arr.forEach(x => result.push(x));  // push 返回 number,但这里 void 接受它

// undefined:函数必须明确 return undefined 或不 return
type UndefinedFn = () => undefined;

function noReturn(): undefined {
  // return;            // 错误
  // return "string";   // 错误
  return undefined;    // 必须显式返回 undefined
}

实际规则:

// 函数声明的 void 返回类型:可以有 return,但必须没有值
function log(msg: string): void {
  console.log(msg);
  return;            // OK
  // return undefined; // OK(等价)
  // return 1;         // 错误
}

// 类型别名中的 void 更宽松(为了兼容回调)
const fn: () => void = () => 42;  // OK!

// 对比:undefined 更严格
const fn2: () => undefined = () => 42;  // 错误:number 不能赋值给 undefined

Call Signature:有属性的可调用对象

当你需要一个既可以调用又有属性的对象时,使用 call signature:

// 普通函数类型无法有属性
// type Logger = ((msg: string) => void) & { level: "info" | "error" };
// 用起来可以,但 call signature 更清晰:

interface Logger {
  (message: string): void;         // call signature
  level: "info" | "error" | "warn";
  prefix: string;
}

function createLogger(level: Logger["level"], prefix: string): Logger {
  const fn = ((message: string) => {
    console.log(`[${fn.prefix}][${fn.level}] ${message}`);
  }) as Logger;

  fn.level = level;
  fn.prefix = prefix;

  return fn;
}

const logger = createLogger("info", "APP");
logger("Server started");          // 调用
console.log(logger.level);         // "info" — 访问属性

带多个 call signature 的对象(实现重载):

interface Formatter {
  (value: string): string;
  (value: number): string;
  locale: string;
}

const fmt = ((value: string | number) => {
  return typeof value === "number" ? value.toFixed(2) : value.trim();
}) as Formatter;

fmt.locale = "en-US";

fmt("  hello  ");   // "hello"
fmt(3.14159);       // "3.14"

Level 3 · 规范怎么定义的(资深)

TypeScript 的函数重载与 Java/C# 的重载有本质区别:TypeScript 的重载只存在于类型层面,运行时只有一个实现函数。重载签名的匹配顺序是从上到下,编译器选择第一个匹配的签名。void 的双重语义是 TypeScript 为兼容 JavaScript 回调模式做出的设计妥协——在类型别名中 () => void 接受任何返回值,是为了让 Array.prototype.forEach 等接受返回值被忽略的回调;在函数声明中 void 更严格,不允许返回具体值。

Level 4 · 边界与陷阱(所有人)

实现签名不可直接调用:这是函数重载最常见的误解。外部调用者只能看到重载签名,实现签名对外不可见。

不该用重载时用了重载:如果不同参数组合返回的类型相同,直接用联合类型参数更简洁。重载只在"不同输入 -> 不同输出"时才有价值。

this 参数类型在回调中丢失:将对象方法作为回调传递时,this 绑定会丢失。使用箭头函数或显式 this 参数类型来防护。


本章要点总结

特性 要点 示例
函数类型语法 箭头语法 vs 对象语法 (a: T) => R vs { (a: T): R }
可选参数 ? 让参数变为 T | undefined (x?: number)
默认值 调用方传 undefined 触发默认值 (x = 0)
剩余参数 类型是数组或元组 (...args: number[])
重载 实现签名不对外,联合能解决时不用重载 多个签名 + 一个实现
泛型函数 保留类型信息,用约束限制范围 <T extends U>
this 参数 假参数,编译后消失,防止错误上下文调用 (this: MyClass)
void vs undefined void 忽略返回值;undefined 要求明确返回 undefined 回调用 void,严格函数用 undefined
Call signature 让对象既可调用又有属性 interface Fn { (x: T): R; prop: P }

下一章介绍泛型的进阶用法:泛型类、泛型约束、条件类型与 infer 关键字。

本章评分
4.9  / 5  (64 评分)

💬 留言讨论