TypeScript 函数类型系统详解:参数注解、重载与函数类型声明(深入理解 TypeScript 实战指南)

原创2026-10-07 20:48:531,752 阅读
文章标签:文档教程

TypeScript 函数类型系统详解:参数注解、重载与函数类型声明(深入理解 TypeScript 实战指南)

本文基于《深入理解 TypeScript》(typescript-book-chinese)中 docs/typings/functions.md 展开,函数类型是 TypeScript 类型系统中可组合系统的核心构建块。读完本文,你将掌握:如何为函数参数与返回值添加类型注解、如何利用可选参数与默认参数写出更宽容的 API、如何用函数重载在零运行时开销的前提下强制并记录调用约束,以及如何用长格式(LongHand)与短格式(ShortHand)两种语法声明函数类型,并区分它们各自的能力边界。

参数注解:与变量注解同源的语法

在 TypeScript 中,注解函数参数的方式与注解普通变量完全一致。你可以直接使用内联类型注解,也可以借助 interface、type 别名等更高层的抽象:

// variable annotation
let sampleVariable: { bar: number };

// function parameter annotation
function foo(sampleParameter: { bar: number }) {}

内联对象类型注解适合快速表达"一个包含 bar: number 字段的对象";当参数结构变得复杂,或需要在多处复用时,更推荐抽出为 interface(详见 docs/typings/interfaces.md)或类型别名。两种写法的类型检查行为一致,选择标准是可读性与复用性。

这种"参数即类型"的设计,让函数签名本身就成为 API 的文档——调用方在 IDE 中即可看到参数结构,无须翻看函数实现。

返回类型注解:可显式、可推断、可省略

你可以在参数列表之后,用与变量相同的样式注解返回类型:

interface Foo {
  foo: string;
}

// Return type annotated as `: Foo`
function foo(sample: Foo): Foo {
  return sample;
}

这里使用了 interface,但你完全可以改用内联注解等其他方式。

通常不需要显式注解:让编译器推断

大多数情况下,返回类型可以由编译器自动推断,无须手工标注:

interface Foo {
  foo: string;
}

function foo(sample: Foo) {
  return sample; // inferred return type 'Foo'
}

但显式注解能帮你提前发现拼写错误

推断虽然方便,却可能掩盖问题。例如下面这段代码,return 的对象里把 foo 拼成了 fou,如果没有返回类型约束,错误要到下游消费 foo() 返回值时才可能暴露:

function foo() {
  return { fou: 'John Doe' }; // You might not find this misspelling of `foo` till it's too late
}

sendAsJSON(foo());

如果为 foo 显式声明返回类型 : { foo: string },编译器会立刻在函数体内报错,把"运行时才发现的问题"提前到编码阶段。因此实践上,对公共 API 建议显式标注返回类型,以帮助解决错误提示并稳定对外契约。

无返回值:标注 void

如果不打算从函数返回任何内容,可将其标注为 void。void 通常也可以省略,因为 TypeScript 能自行推导出来,但显式写出能向读者表明"此函数不产生返回值"的意图:

function log(message: string): void {
  console.log(message);
}

可选参数与默认参数

可选参数:? 后缀

你可以把参数标记为可选,调用时传或不传均可:

function foo(bar: number, bas?: string): void {
  // ..
}

foo(123);
foo(123, 'hello');

默认参数:= someValue

更常见的做法是当调用者没有提供该参数时,提供一个默认值(在参数声明后使用 = someValue):

function foo(bar: number, bas: string = 'hello') {
  console.log(bar, bas);
}

foo(123); // 123, hello
foo(123, 'world'); // 123, world

两者都让函数对外更宽容:可选参数表达"这个参数可有可无",默认参数则进一步把"缺失"归一化为一个确定值。注意,默认参数在编译后会生成对应的运行时赋值逻辑,而可选参数仅仅是类型层面的放宽。

函数重载:强制并记录调用约束

TypeScript 允许你声明函数重载。这对"文档 + 类型安全"来说都很实用。先看一个不加约束的经典例子——一个按传入参数个数改变行为的 padding 函数:

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
  };
}

仔细查看代码可以发现,a、b、c、d 的值会根据传入的参数数量而变化:传 1 个参数表示四个方向等值,传 2 个参数表示上下、左右分别取值,传 4 个参数表示各自独立。该函数实际上只允许 1 个、2 个或 4 个参数。如果不加约束,调用 padding(1, 1, 1)(3 个参数)也能通过编译,行为却含糊不清。

用重载声明约束调用形态

你可以用函数重载来强制和记录这些约束:只需多次声明函数头。最后一个函数头是函数体内实际处于活动状态的声明,但它对外部不可见,外部调用方只能看到前几个重载签名:

// 重载
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// Actual implementation that is a true representation of all the cases the function body needs to handle
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(1); // Okay: all
padding(1, 1); // Okay: topAndBottom, leftAndRight
padding(1, 1, 1, 1); // Okay: top, right, bottom, left

padding(1, 1, 1); // Error: Not a part of the available overloads

padding(1, 1, 1) 会得到 "Not a part of the available overloads" 的编译错误——这正是重载的价值:编译器替你检查调用形态是否符合约定。

实现签名必须与所有重载兼容

最后那个"真正声明"(实现签名)必须与所有重载兼容,这一点非常重要。因为它是函数体需要面对的真实调用集合:函数体收到的可能是一参数、二参数或四参数形态,所以实现签名里的参数必须全部声明为可选(b?、c?、d?),并在函数体内逐一判断。

::: tip 零运行时开销 TypeScript 中的函数重载没有任何运行时开销。它只允许你记录希望调用函数的方式,并且编译器会检查其余代码。重载完全是一个编译期概念,编译产物与普通函数无异。 :::

重载在类型层面的另一种表达

除了 function 重载,你还可以在函数类型中表达重载能力——这正是下面"长格式"声明所擅长的事情。

函数声明:LongHand 与 ShortHand 两种语法

快速开始:类型注解是你描述现有实现类型的一种方式。

在没有提供函数实现的情况下,有两种声明函数类型的方式:

type LongHand = {
  (a: number): number;
};

type ShortHand = (a: number) => number;

上面两个类型完全等价,都表示"接收一个 number 参数并返回 number 的函数"。短格式(ShortHand)用箭头函数语法,直观简洁,日常使用频率最高;长格式(LongHand)采用对象字面量 + 调用签名的形式,与 docs/typings/callable.md 中介绍的"可调用接口"同一思路。

重载声明:只能用 LongHand

两者的能力边界在于:当你想在类型中声明函数重载时,只能使用第一种(LongHand)方式:

type LongHandAllowsOverloadDeclarations = {
  (a: number): number;
  (a: string): string;
};

短格式((a: number) => number)只是一条调用签名,无法叠加多条。如果你需要一个"按参数类型分派返回类型"的函数类型,请使用长格式或可调用接口(详见 docs/typings/callable.md 中的 Overloaded 接口示例)。

可调用接口与可实例化类型

与长格式同源的还有"可调用接口",以及它的特殊形式"可实例化类型"(以 new 为前缀):

interface CallMeWithNewToGetString {
  new (): string;
}

declare const Foo: CallMeWithNewToGetString;
const bar = new Foo(); // bar 被推断为 string 类型

这表明:函数类型声明体系并非只覆盖普通调用,还能精确描述"必须用 new 实例化"的构造器形态。

延伸:函数类型与类型兼容性

给函数写类型只是第一步,理解两个函数类型何时互相兼容才是组合系统的关键。docs/typings/typeCompatibility.md 给出了完整的函数兼容规则:

  • 返回类型协变:返回类型必须包含足够的数据。(): Point3D 可以赋值给 (): Point2D,反向则报错。
  • 参数数量:更少的参数数量是好的——函数可以选择性地忽略多余的参数,但至少要保证它实际用到的参数都被提供。
  • 参数类型双向协变(Bivariant):TypeScript 出于支持常见事件处理场景的便利性,允许函数参数类型双向协变。这并不完全安全,但可以通过 --strictFunctionTypes(或 --strict)收紧。相关背景详见 docs/tips/covarianceAndContravariance.md。
  • 可选参数与 rest 参数:可选的(预先确定的)与 rest 参数(任意数量的参数)是兼容的,但可选与不可选参数的互赋仅在 strictNullChecks 为 false 时成立。

这些规则决定了你在编写回调、事件处理器和高阶函数时的赋值是否合法,是函数类型知识的自然延伸。

实战建议:何时该重构函数签名

当函数拥有很多参数,或包含多个相同类型的参数时,容易在调用处把参数顺序搞错。例如:

function foo(flagA: boolean, flagB: boolean) {
  // 函数主体
}

调用 foo(flagB, flagA) 不会得到编译器的任何帮助,因为两个 boolean 完全同型。此时可改为接收对象的形式(详见 docs/tips/functionParameters.md):

function foo(config: { flagA: boolean; flagB: boolean }) {
  const { flagA, flagB } = config;
}

调用变为 foo({ flagA, flagB }),命名让意图显式化,有利于发现错误和代码审查;配合上面的"参数注解"能力,对象参数还能直接享受 IDE 的结构提示。当然,如果你的函数足够简单,不希望增加代码,可以忽略这个建议。

小结

围绕 docs/typings/functions.md,本文完整覆盖了函数类型系统的四条主线:

  1. 参数注解:内联注解与 interface 等价,按需选用;
  2. 返回类型:优先让编译器推断,公共 API 建议显式标注,无返回值用 void;
  3. 可选参数与默认参数:分别从类型与运行时两个层面放宽调用约束;
  4. 重载与函数类型声明:用重载强制并记录调用形态(零运行时开销);声明函数类型时短格式简洁、长格式可表达重载。

将函数视为"可组合系统的核心构建块"(文档开篇的定位),再配合 docs/typings/callable.md 的可调用/可实例化接口、docs/typings/typeCompatibility.md 的兼容性规则,你就能在项目中写出既安全又灵活的 TypeScript 函数 API。

登录后查看全文
typescript-book-chinese