大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
函数类型
这一课你将能做到
- 给函数声明、函数表达式、箭头函数标注完整的参数和返回值类型
- 正确使用可选参数、默认参数、剩余参数,并理解它们在类型层面的表现
- 用函数类型签名和重载描述「同一个函数多种调用方式」的场景
为什么要学
函数是 JavaScript 的一等公民,也是 TS 类型注解出现频率最高的地方。你写的每一个回调、map/filter 的入参、React 的事件处理函数,背后都是函数类型。
函数类型标注有个核心思想:函数的类型 = 输入(参数)+ 输出(返回值)。把这两个写准,TS 就能帮你检查所有调用点。函数参数是类型推断唯一「推不出来」的地方——所以函数参数必须手写注解,这是 TS 世界的一条铁律。
先建立图像
一个完整函数类型的解剖:
参数名 参数类型 返回值类型
│ │ │
function add(a: number, b: number): number {
return a + b;
}
│
函数名
整个函数的类型签名:(a: number, b: number) => number
参数列表 返回值
注意类型签名里的箭头 => 和箭头函数的 => 长得像但含义不同:前者出现在类型位置,表示「返回」;后者出现在值位置,是函数定义语法。
跟着做
新建 functions-demo.ts,每步代码可直接 tsc 编译运行。
步骤 1:函数声明与函数表达式的标注
// 函数声明:参数和返回值都标在函数头上
function multiply(a: number, b: number): number {
return a * b;
}
// 函数表达式:两种方式
// 方式 A:只标函数本身(推荐,简洁)
const divide = function (a: number, b: number): number {
return a / b;
};
// 方式 B:给变量标注完整函数类型(赋值给配置、接口时常用)
const subtract: (a: number, b: number) => number = function (a, b) {
return a - b; // 参数类型从签名推断,不用重复写
};
console.log(multiply(3, 4), divide(10, 2), subtract(10, 3));
// 12 5 7
步骤 2:箭头函数与返回值推断
// 显式标注返回值
const square = (n: number): number => n * n;
// 返回值可推断时省略(推荐)
const double = (n: number) => n * 2; // TS 推断返回 number
// 无返回值
const log = (msg: string): void => {
console.log(msg);
};
const nums = [1, 2, 3].map(double); // 回调参数 n 自动推断为 number
console.log(nums, square(5)); // [ 2, 4, 6 ] 25
经验:返回值类型大多数时候让 TS 推断;只有两种情况下手写——函数体复杂想给读者看契约、或要强制返回值符合某类型(写错能当场报错)。
步骤 3:可选参数与默认参数
// 可选参数:参数名后加 ?
function greet(name: string, greeting?: string): string {
return `${greeting ?? "你好"},${name}`;
}
console.log(greet("小明")); // 你好,小明
console.log(greet("小明", "早上好")); // 早上好,小明
// 默认参数:直接写默认值,类型自动推断,调用时可省略
function createUser(name: string, role: string = "viewer"): string {
return `${name}(${role})`;
}
console.log(createUser("小红")); // 小红(viewer)
console.log(createUser("管理员", "admin")); // 管理员(admin)
规则记住两条:可选参数的类型实际是 类型 | undefined;可选参数必须放在必选参数后面。
步骤 4:剩余参数
// 剩余参数:把不定数量的实参收集成数组,类型写成「元素类型[]」
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4)); // 10
// 剩余参数前面可以有固定参数
function joinWith(separator: string, ...parts: string[]): string {
return parts.join(separator);
}
console.log(joinWith("-", "2026", "10", "06")); // 2026-10-06
步骤 5:回调函数的类型
// 定义一个回调类型签名
type CompareFn = (a: number, b: number) => number;
function sortWith(list: number[], compare: CompareFn): number[] {
return [...list].sort(compare);
}
const asc = sortWith([3, 1, 2], (a, b) => a - b);
const desc = sortWith([3, 1, 2], (a, b) => b - a);
console.log(asc, desc); // [ 1, 2, 3 ] [ 3, 2, 1 ]
type CompareFn = (a: number, b: number) => number 就是「函数类型别名」——这是实际项目里描述回调的标准姿势。
步骤 6:函数重载
当同一个函数有多种调用形态(参数个数或类型不同、返回值跟着变),用重载:
// 重载签名(只写类型,不写函数体)
function format(value: string): string;
function format(value: number, precision: number): string;
// 实现签名(兼容所有重载,函数体写这里)
function format(value: string | number, precision?: number): string {
if (typeof value === "string") {
return value.trim();
}
return value.toFixed(precision ?? 2);
}
console.log(format(" hello ")); // "hello"
console.log(format(3.14159, 2)); // "3.14"
// console.log(format(true)); // 报错:没有匹配的重载
要点:重载签名在外,实现签名在内;调用时只能匹配重载签名,实现签名对外不可见。
步骤 7:综合实战——一个类型完整的事件发射器
type Handler = (payload: string) => void;
class Emitter {
private handlers: Handler[] = [];
on(handler: Handler): void {
this.handlers.push(handler);
}
emit(payload: string): void {
for (const handler of this.handlers) {
handler(payload);
}
}
}
const emitter = new Emitter();
emitter.on((msg) => console.log("监听器1:", msg));
emitter.on((msg) => console.log("监听器2:", msg.toUpperCase()));
emitter.emit("hello ts");
// 监听器1: hello ts
// 监听器2: HELLO TS
讲清楚要点
- 参数必须注解,返回值可推断:这是 TS 函数标注的不对称规则。参数推不出来(没人告诉你调用方会传什么),返回值得出来(函数体写得明明白白)。
- 可选参数
?等价于| undefined:greeting?: string就是greeting: string | undefined,且自动获得「可省略」能力。 - 默认参数自带可选属性:有默认值的参数调用时可省略,类型从默认值推断,不要再叠加
?。 - 剩余参数永远是数组:
...args: number[]收集成一个真正的数组,放在参数列表最后。 - 重载是「签名堆叠」:写多个类型签名 + 一个兼容实现。能用一个联合类型签名解决的,就别上重载——重载只在「参数形态决定返回值类型」时必要。
- 函数类型也是值:函数可以赋给变量、当参数传、当返回值返回,类型签名
(a: T) => R就是它的「形状描述」。
你可能会懵的地方
-
箭头函数怎么写完整类型?
(x: number) => number是类型,const f = (x: number) => x * 2是实现。想分开写:const f: (x: number) => number = (x) => x * 2,实现里的参数类型可以省略(从签名推断)。 -
可选参数和
| undefined完全一样吗? 不一样。a?: string调用时可以不传;a: string | undefined必须显式传undefined。前者更常用。 -
为什么回调函数里的参数可以不写类型? 上下文推断(contextual typing):TS 知道
arr.map的回调收number,就自动给回调参数标上。前提是这个位置的类型定义得好——这正是写库的人认真标注函数类型的意义。 -
重载和联合类型参数选哪个? 参数类型不同但返回值类型不变 → 用联合类型一个签名搞定;返回值类型随参数形态变化 → 才用重载。
-
void返回的函数能return 值吗? 严格说return someValue会报错,但return空返回可以。特殊例外:把一个返回值的函数赋给void类型的回调是允许的(比如forEach的回调),TS 故意放宽,避免连锁报错。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
function f(a, b) { ... } | 隐式 any,strict 下直接报错 | 参数逐个标注类型 |
可选参数放必选前面 (a?: string, b: number) | 编译错误 | 必选在前,可选在后 |
默认参数还加 ?:f(a?: number = 1) | 语法错误,两者重复表达「可省略」 | f(a: number = 1) |
给回调硬标 any:arr.map((x: any) => ...) | 丢掉上下文推断红利 | 省略参数类型让 TS 推 |
| 重载实现签名写得太窄 | 重载签名与实现不兼容报错 | 实现签名用联合类型覆盖所有分支 |
卡点急救
- 报错
Parameter 'x' implicitly has an 'any' type:strict 模式的noImplicitAny生效了,给参数补上类型注解。 - 可选参数调用时传
undefined报错:检查函数定义是a?: T还是a: T;后者必须传值。改成可选参数或T | undefined。 - 重载调用报
No overload matches this call:调用形态没匹配上任何重载签名。检查参数类型和个数;或确认实现签名是否真的处理了这种情况。 - 回调里
this丢失:用了普通函数当回调导致this漂移。改用箭头函数,或在签名里声明this: void。
面试常问
-
函数类型注解的完整写法?
(参数名: 类型, ...) => 返回值类型。可赋给变量:const f: (a: number) => number = ...。参数类型不可省,返回值可被推断。 -
可选参数和默认参数的区别? 可选参数
?:不传时是undefined,函数体内要处理;默认参数:不传时用默认值,函数体内拿到的永远是有值的。默认参数类型从默认值推断。 -
什么是函数重载?TS 和 Java 的重载有何不同? TS 重载是多个类型签名 + 一个实现,仅在编译期生效,运行时只有一个函数;Java 重载是多个真实方法共存。TS 重载的目的是给不同调用形态配不同的返回类型。
-
void和undefined作为返回类型的区别?void表示「不期待使用返回值」,赋给它的函数即使 return 了值也被接受(回调场景);undefined要求函数必须显式return undefined。描述函数返回用void。
自测三问
- 为什么函数参数必须手写类型而返回值通常不用?
function f(a?: string)和function f(a: string | undefined)调用时有什么区别?- 什么场景必须用函数重载,什么场景用联合类型参数就够了?
小结一句话
函数类型的核心是「输入输出契约」:参数必标、返回值可推断、可选/默认/剩余参数处理变化、重载描述多形态——契约写清楚,所有调用点都有编译器站岗。
延伸
- Handbook: More on Functions
- 下一课:接口与类型别名——
interface和type怎么选、可选/只读属性、索引签名、继承与交叉。