TypeScript 函数类型系统完全指南:参数注解、返回类型、可选参数与重载实战
TypeScript 函数类型系统完全指南:参数注解、返回类型、可选参数与重载实战
函数是任何可组合系统的核心构建块,TypeScript 的类型系统也为函数投入了极大的关注。本篇指南以本仓库 docs/types/functions.md 为主体,结合仓库中可运行的示例源码 code/types/functions.ts 与编译产物 code/types/functions.js,系统讲解函数的参数注解、返回类型注解、可选参数与默认值、函数重载,以及如何用类型别名声明函数类型。读完本篇,你将掌握为函数精确建模的完整能力,并理解这些类型注解如何在编译期约束代码、在运行时零开销。
参数注解:为函数入参建立类型契约
与普通变量的注解方式一样,你可以为函数参数添加类型注解。TypeScript 支持两种写法:内联类型注解(inline annotation)与命名接口(interface)。
// 变量注解
var sampleVariable: { bar: number }
// 函数参数注解(内联类型)
function foo(sampleParameter: { bar: number }) { }
内联注解直接写在参数声明处,适合一次性使用的简单结构。当结构需要在多处复用、或者希望通过 implements 约束类实现时,更推荐使用接口:
interface Bar {
bar: number;
}
function foo(sampleParameter: Bar) { }
关于接口的更多能力(开放扩展、implements 约束等),可参考仓库文档 docs/types/interfaces.md。在仓库示例 code/types/functions.ts 中,parameter 命名空间完整演示了变量注解与参数注解的对应关系。
返回类型注解:显式声明输出
返回类型注解的语法与变量注解一致,写在参数列表之后,用 : 类型 表示。例如下面的 foo 接受一个 Foo 并返回 Foo:
interface Foo {
foo: string;
}
// 返回类型注解为 `: Foo`
function foo(sample: Foo): Foo {
return sample;
}
这里使用了接口,但你同样可以使用内联注解等其他形式。
返回类型的推断
大多数情况下,你并不需要显式标注返回类型,因为编译器可以从 return 语句推断出来:
interface Foo {
foo: string;
}
function foo(sample: Foo) {
return sample; // 推断返回类型为 'Foo'
}
这条规则与仓库文档 docs/types/type-inference.md 中"函数返回类型由 return 语句推断"的规则一致:function add(a: number, b: number) { return a + b; } 会被推断为返回 number。
显式返回注解的价值:尽早暴露拼写错误
尽管推断很智能,显式添加返回类型注解仍然是良好实践,因为它能帮助编译器在错误蔓延之前抓住问题:
function foo() {
return { fou: 'John Doe' }; // `fou` 是 `foo` 的拼写错误,可能很久之后才会被发现
}
sendAsJSON(foo());
没有返回类型注解时,foo() 的返回类型会被推断为 { fou: string },而调用方 sendAsJSON 直到真正出错前都不会察觉字段名错误。仓库示例 code/types/functions.ts 中 misspelled 命名空间正是这段反例的完整演示。
推荐实践:始终对函数返回类型保持显式。类型注解如同一份定理声明,而函数体就是它的证明。
无返回值函数:: void
如果不打算从函数返回任何东西,可以将其注解为 : void:
function foo(bar: number, bas?: string): void {
// ..
}
通常你也可以省略 : void,交由推断引擎处理,两种写法在语义上等价。
可选参数与默认参数:灵活但安全的入参
JavaScript 函数天然允许少传参数,TypeScript 通过 ? 标记让这种灵活性变得类型安全。将参数标记为可选后,调用方可以省略该实参:
function foo(bar: number, bas?: string): void {
// ..
}
foo(123);
foo(123, 'hello');
可选的另一个替代方案是提供默认值:在参数声明后使用 = someValue,当调用方未传入该实参时,默认值会被自动注入:
function foo(bar: number, bas: string = 'hello') {
console.log(bar, bas);
}
foo(123); // 输出: 123, hello
foo(123, 'world'); // 输出: 123, world
仓库示例 code/types/functions.ts 的 optional 与 optionalDefault 两个命名空间分别对应上述两种写法。值得一提的是,默认参数是纯编译期注入——查看编译产物 code/types/functions.js,可见 bas = 'hello' 被编译为:
function foo(bar, bas) {
if (bas === void 0) { bas = 'hello'; }
console.log(bar, bas);
}
即运行时通过 void 0 判断实参是否为 undefined 来注入默认值,这正是 ECMAScript 默认参数语义的标准降级实现。这个仓库配置为 ES5 目标(见 code/types/tsconfig.json),因此 TypeScript 编译器需要自己完成默认参数的降级转换。
函数重载:声明多个合法调用形态
函数重载是 TypeScript 表达"一个函数可按多种方式调用"的利器,兼具文档化与类型安全双重价值。
先看一个没有重载的朴素实现——一个计算内边距(padding)的函数,a、b、c、d 四个参数的含义会随传入参数个数而变化:
function padding(a: number, b?: number, c?: number, d?: any) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a;
}
else if (c === undefined && d === undefined) {
c = a;
d = b;
}
return {
top: a,
right: b,
bottom: c,
left: d
};
}
仔细阅读这段代码你会发现:传入 1 个参数时表示四周等距(all);传入 2 个参数时表示上下、左右两组值(topAndBottom / leftAndRight);传入 4 个参数时分别表示 top、right、bottom、left。该函数实际上只接受 1、2 或 4 个参数。这些约束可以通过重载强制执行并记录下来:只需多次声明函数头(签名),其中最后一个函数头是函数体内部真正生效的实现,但它对外部世界不可见。
// 重载签名
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// 实际实现:对函数体需要处理的所有情况做真实表达
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a;
}
else if (c === undefined && d === undefined) {
c = a;
d = b;
}
return {
top: a,
right: b,
bottom: c,
left: d
};
}
现在,前三个函数头成为对 padding 的合法调用形态:
padding(1); // 可以: all
padding(1, 1); // 可以: topAndBottom, leftAndRight
padding(1, 1, 1, 1);// 可以: top, right, bottom, left
padding(1, 1, 1); // 错误: 不属于可用的重载之一
关键在于:最后一个声明(函数体内可见的真实声明)必须与所有重载兼容,因为函数体需要应对真实函数调用的全部形态。在仓库示例 code/types/functions.ts 中,overloads(无重载版)与 overloadsDone(带重载版)两个命名空间并存,便于对照学习;两者编译后的 JavaScript 产物(code/types/functions.js)完全一致,这印证了下面的关键事实:
TypeScript 的函数重载不带来任何运行时开销。它只是让你以声明的方式记录函数预期的调用方式,而编译器负责约束你其余代码的写法。
换言之,重载是纯粹的编译期特性——重载签名在编译产物中被完全擦除,只剩一个实现。
重载与可调用接口的对照
重载能力并非函数声明独有。仓库文档 docs/types/callable.md 展示了在接口中声明多个调用签名来实现同样的多形态约束:
interface Overloaded {
(foo: string): string
(foo: number): number
}
配套源码 code/types/callable.ts 演示了如何实现并消费这种可重载接口。这说明:无论用函数重载还是可调用接口,"一个调用点有多个合法签名"的类型建模思想是一致的。
声明函数类型:LongHand 与 ShortHand
快速提示:类型声明(Type Declarations) 是你用来描述既有实现类型的方式。
在不提供实现的前提下声明函数类型有两种写法,它们完全等价:
type LongHand = {
(a: number): number;
};
type ShortHand = (a: number) => number;
两者的差异在需要添加重载时显现:重载只能写在 LongHand(长手)版本中:
type LongHandAllowsOverloadDeclarations = {
(a: number): number;
(a: string): string;
};
这与 docs/types/callable.md 中"箭头语法(ShortHand)的唯一限制:无法声明重载,重载必须使用完整的 { (someArgs): someReturn } 语法"的结论完全对应。仓库示例 code/types/callable.ts 还演示了将这种内联调用签名直接用作变量注解:
const overloaded: {
(foo: string): string
(foo: number): number
} = (foo) => foo;
进阶:可调用类型与构造签名(newable)
既然函数类型的本质是一组调用签名,那么接口/类型中还可以组合更多形态。仓库文档 docs/types/callable.md 与源码 code/types/callable.ts 展示了带可选参数、rest 参数的复杂调用签名:
interface Complex {
(foo: string, bar?: number, ...others: boolean[]): number;
}
以及需要以 new 调用的"可构造"类型(newable):
interface CallMeWithNewToGetString {
new(): string
}
// 使用
declare const Foo: CallMeWithNewToGetString;
const bar = new Foo(); // bar 被推断为 string 类型
这些能力共同构成了 TypeScript 对 JavaScript 中"函数即一等公民"这一现实的完整建模。
与其他类型主题的衔接
函数类型系统并非孤立存在,它与本仓库 docs/types 目录下的多个主题紧密相关:
- 类型推断:返回类型由
return语句推断、函数参数类型可通过赋值上下文(如回调参数)推断,详见 docs/types/type-inference.md; - 可调用接口:用接口声明调用签名、newable 构造签名,详见 docs/types/callable.md;
- 接口:开放扩展、
implements约束,详见 docs/types/interfaces.md; - 类型兼容性:函数参数与返回类型在赋值兼容性(协变/逆变)中的行为,详见 docs/types/type-compatibility.md;
- 类型守卫:通过守卫收窄函数内参数类型,详见 docs/types/typeGuard.md;
- 箭头函数:
this/arguments的词法捕获及其注意事项,详见 docs/arrow-functions.md; - noImplicitAny:当函数参数无法推断类型时,编译器会报错提示你显式注解,详见 docs/options/noImplicitAny.md。
动手实践
仓库为每个示例都提供了可运行的 TypeScript 源文件与编译后的 JavaScript 产物,你可以对照学习:
- 源文件:code/types/functions.ts —— 包含参数注解、返回类型、可选参数、默认参数、重载共 7 组命名空间示例;
- 编译产物:code/types/functions.js —— 直观展示类型注解、重载签名被完全擦除,默认参数如何降级为
void 0判断; - 相关配置:code/types/tsconfig.json —— 以 ES5 为目标编译,
noImplicitAny关闭,便于聚焦函数类型语法本身。
建议的验证方式:将 padding 的非法调用 padding(1, 1, 1) 取消注释,运行 tsc 即可看到编译器报出的"不属于可用重载"错误,从而直观体会函数重载的约束力;随后查看编译产物,确认重载签名没有产生任何额外运行时代码。