函数类型:重载、泛型函数、call signature
第5章:函数类型:重载、泛型函数、call signature
TypeScript 中函数类型的完整表达能力:重载、泛型、this 参数、void 语义各有什么规则?这是理解本章内容的出发点。
本章核心问题:TypeScript 中函数类型的完整表达能力:重载、泛型、this 参数、void 语义各有什么规则?
读完本章你将理解:
- 两种函数类型语法及各自适用场景
- 函数重载的正确使用时机(不同参数组合对应不同返回类型)
void和undefined的微妙差别及 call signature 的用法
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:微妙的差别
void 和 undefined 看起来相似,但含义不同:
// 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 关键字。