TypeScript 可调用类型(Callable Types)完全指南:调用签名、函数重载与可实例化注解

原创2026-10-07 22:55:58710 阅读
文章标签:文档教程

TypeScript 可调用类型(Callable Types)完全指南:调用签名、函数重载与可实例化注解

导读

本文聚焦《深入理解 TypeScript(中文版)》中「可调用的」一节(docs/typings/callable.md),系统讲解 TypeScript 中如何用接口、类型别名和内联对象字面量来注解"可以被调用"的值——从最简单的函数签名,到带可选参数、rest 参数与重载的复杂签名,再到使用 new 前缀的"可实例化"签名。读完本文,你将掌握函数类型注解的全部书写形态、重载签名的正确姿势、箭头函数类型的边界,以及 new 签名的真实应用场景(如混合类与构造器类型推导)。

什么是可调用类型

在 TypeScript 的类型系统中,一个值能否被"调用"(即当作函数执行 ())也是一种类型约束。你可以使用类型别名(type alias)或接口(interface)来表示一个可被调用的类型注解。最基本的写法是在接口体内直接声明一个不带名字的方法签名(即调用签名 / call signature):

interface ReturnString {
  (): string;
}

这里的 (): string 就是调用签名:它表示"这是一个可被调用的值,调用它不需要任何参数,返回 string"。

它可以用 declare const 声明一个符合该类型的值,并安全地调用它:

declare const foo: ReturnString;

const bar = foo(); // bar 被推断为一个字符串

在 docs/typings/typeInference.md 中可以看到这种推断的底层机制:函数调用表达式的返回类型由调用签名决定,因此 bar 的类型会被精确推断为 string,后续对 bar 的赋值、使用都会受到 string 类型的约束。

一个实际的例子:复杂签名与参数类型

可调用类型绝不限于"零参返回 string"这种简单形态。你可以根据实际需要传递任何参数、可选参数以及 rest 参数。下面的 Complex 接口声明了一个接受三个参数(一个必填 string、一个可选 number、一组 rest 的 boolean[])并返回 number 的调用签名:

interface Complex {
  (foo: string, bar?: number, ...others: boolean[]): number;
}
  • foo: string:必填参数;
  • bar?: number:可选参数,调用时可以省略;
  • ...others: boolean[]:rest 参数,收拢剩余实参为一个布尔数组。

这与普通函数声明中的参数规则完全一致(参考 docs/typings/functions.md 中"可选参数"一节对 bas?: string 和默认值参数 bas: string = 'hello' 的讨论),因此任何合法的参数形态都可以出现在调用签名中。

一个接口,多种调用签名:函数重载

一个接口可以提供多种调用签名,用于表达特殊的函数重载。当同一个函数依据入参类型不同而返回不同类型时,用多个调用签名可以精确描述这一行为:

interface Overloaded {
  (foo: string): string;
  (foo: number): number;
}

要实现这个接口,你需要写一个与之匹配的实现函数。注意实现的方式是声明两个重载签名(string 版本与 number 版本),再用一个宽泛的实现签名承载函数体:

// 实现接口的一个例子:
function stringOrNumber(foo: number): number;
function stringOrNumber(foo: string): string;
function stringOrNumber(foo: any): any {
  if (typeof foo === 'number') {
    return foo * foo;
  } else if (typeof foo === 'string') {
    return `hello ${foo}`;
  }
}

const overloaded: Overloaded = stringOrNumber;

// 使用
const str = overloaded(''); // str 被推断为 'string'
const num = overloaded(123); // num 被推断为 'number'

在这里,stringOrNumber 的重载签名与 Overloaded 接口的多个调用签名一一对应,TypeScript 会依据实参类型(string 还是 number)为调用结果选择正确的返回类型:传入字符串得到 string,传入数字得到 number。这与 docs/typings/functions.md 中 padding 函数按参数个数重载的模式一致——重载签名对"外部可见的调用方式"进行约束与文档化,实现签名(内部真正生效的那个)只需与所有重载兼容。

重载也可用于内联注解

同样的多调用签名语法可以直接写在内联类型注解中,无需命名接口:

let overloaded: {
  (foo: string): string;
  (foo: number): number;
};

箭头函数类型

为了让指定可调用类型签名更加容易,TypeScript 允许使用简单的箭头函数类型注解。例如:一个以 number 为参数、以 string 为返回值的函数,可以写成:

const simple: (foo: number) => string = foo => foo.toString();

这与 docs/typings/functions.md 中"函数声明"一节给出的两种等价写法一致:

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

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

长写法与短写法完全等价。

::: tip 注意 箭头函数类型仅仅只能表达简单的单签名函数,无法使用重载。如果想用重载,你必须使用完整的 { (someArgs): someReturn } 语法(即长写法),把多个调用签名依次列出。 :::

换句话说,(foo: number) => string 只能描述一种调用形态;一旦需要"同一函数按入参类型不同返回不同类型",就回到上一节的多调用签名写法。

可实例化:new 前缀的调用签名

可实例化(constructable)仅仅是可调用的一种特殊情况,区别在于它使用 new 作为前缀——意味着你需要使用 new 关键字去调用它:

interface CallMeWithNewToGetString {
  new (): string;
}

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

这里的 new (): string 表示"这是一个构造器,new 调用它时返回 string'"。TypeScript 对 new Foo() 的返回类型推断同样精确:bar被推断为string`。

从源码看 new 签名的真实应用

new 签名在仓库的多个主题中都有实际应用,可以作为理解它的佐证:

  • 混合(Mixin)模式:在 docs/typings/mixins.md 中,所有 mixins 都依赖一个构造器类型别名:

    type Constructor<T = {}> = new (...args: any[]) => T;
    

    这里用 new (...args: any[]) => T 表达"接受任意参数的构造函数",正是 new 前缀调用签名在类型工具层面的典型用法。

  • infer 提取构造器信息:在 docs/tips/infer.md 中,构造器类型写作 new (...args: any[]) => any,并且 TypeScript 内置了 ConstructorParameters<T> 与 InstanceType<T> 两个映射类型,分别用 new (...args: infer P) => any 与 new (...args: any[]) => infer R 提取构造参数的元组类型与实例类型。

  • 接口描述"疯狂"的 JS 结构:在 docs/typings/interfaces.md 的"并非每个接口都是很容易实现的"一节中,接口可以声明 new (): { hello: number } 来匹配"构造函数返回对象"这类特殊 JavaScript 写法。这说明 new 签名让类型系统能够覆盖 JavaScript 中一切可能被 new 调用的结构——但同时也提醒我们:接口能声明它,不代表你能用 TypeScript 类轻松实现它。

从调用签名到可组合的类型系统

理解了本文的可调用类型之后,你会发现它在整个类型系统中处于枢纽位置:

  • 与类型推断(docs/typings/typeInference.md)结合:函数参数与返回类型可以通过赋值流(如 let foo: Adder = (a, b) => a + b)从调用签名"从右向左"推断出来;
  • 与类型保护(docs/typings/typeGuard.md)结合:重载实现内部常用 typeof / instanceof 收窄入参类型,保证运行时分支与签名一致;
  • 与环境声明(docs/typings/ambient.md)结合:为第三方 JS 库编写 .d.ts 时,函数入口通常用调用签名描述,构造器用 new 签名描述;
  • 与 this 类型(docs/typings/thisType.md)结合:ThisType<T> 标注的上下文类型同样依赖"字面量被调用签名上下文键入"这一机制,才让对象方法中的 this 获得强类型。

小结

场景 推荐写法
单一调用形态 interface ReturnString { (): string } 或 type Fn = (x: number) => string
复杂参数(可选、rest) interface Complex { (foo: string, bar?: number, ...others: boolean[]): number }
函数重载 接口/类型别名内列出多个调用签名,如 { (foo: string): string; (foo: number): number }
构造器 new (...args: any[]) => T 或 interface Ctor { new (): string }

可调用类型是 TypeScript 类型系统中"函数是一等公民"的体现:无论是普通函数、重载函数还是构造函数,都能被类型注解精确描述,从而获得与调用形态一一对应的静态检查。完整的章节与更多示例可回到 docs/typings/callable.md 与 docs/typings/functions.md 继续阅读。

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