TypeScript 函数类型系统完全指南:参数注解、返回类型、可选参数与重载实战

原创2026-09-19 22:42:381,967 阅读
文章标签:教程

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 目录下的多个主题紧密相关:

动手实践

仓库为每个示例都提供了可运行的 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 即可看到编译器报出的"不属于可用重载"错误,从而直观体会函数重载的约束力;随后查看编译产物,确认重载签名没有产生任何额外运行时代码。

登录后查看全文
typescript-book