在 TypeScript 中通过调用签名构建简易 Mini Day.js 克隆与 Logger 中间件

发布: (2026年2月4日 GMT+8 13:15)
5 min read
原文: Dev.to

Source: Dev.to

(请提供您希望翻译的完整文本内容,我将按照要求保留链接、格式和代码块,仅翻译正文部分。)

首先,了解函数是对象

你应该知道 typeof 函数原型的结果是 object —— 我指的是原型,而不是 typeof function 本身。

如果你对原型不熟悉,我强烈建议先阅读相关内容再继续。

我们想要解决的问题

如何对函数进行配置,然后使用这些配置来执行它?

这在某种程度上类似于 闭包,但闭包是通过变量作用域封装状态的。使用调用签名时,状态会暴露在外部——这意味着我们 可以在需要时修改属性

你可能会在像 dayjs 这样的库中或实现日志中间件时遇到这种模式。我们该如何在 TypeScript 中正确定义它?

Source:

TypeScript 中的调用签名

在 JavaScript 中,函数除了可以被调用外,还可以拥有属性。然而,典型的函数类型语法并不允许声明属性。如果我们想描述一个既可调用又带有属性的对象,可以使用 对象类型中的调用签名

调用签名类似于闭包,但关键区别在于状态位于 函数外部,因此可以被修改。

示例 1 — Mini Day.js 克隆

type DayJS = {
  timezone: string;
  description: string;
  (date: string | number | Date): string;
};

const dayjs = function (date: string | number | Date) {
  const tz = dayjs.timezone;

  const formatter = new Intl.DateTimeFormat("en-US", {
    timeZone: tz,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });

  return formatter.format(new Date(date));
} as DayJS;

dayjs.timezone = "Asia/Ho_Chi_Minh";
dayjs.description = "Mini DayJS clone";

console.log(dayjs("2026-02-04"));
// 02/04/2026, 07:00:00 AM

这里,dayjs 同时具备:

  • 像函数一样可调用
  • 通过属性进行配置

这正是 调用签名 所实现的功能。

示例 2 — 日志中间件

现在实现一个可配置属性的日志中间件,例如:

  • isEnabled(基于生产环境)
  • 日志类型(warningerrorinfo
  • 消息负载
type LoggerData = { message: string; metadata?: unknown };

type LoggerType = "warning" | "error" | "info";

type LoggerMiddleware = {
  isEnabled: boolean;
  type: LoggerType;
  (data: LoggerData): void;
};

const initializedSomeService = () => {
  const warningLogger: LoggerMiddleware = function (data) {
    if (!warningLogger.isEnabled) return;

    console.warn(`TYPE:${warningLogger.type}`, { data });
  };

  // 配置属性
  warningLogger.isEnabled = process.env.NODE_ENV === "production";
  warningLogger.type = "warning";

  // 像函数一样执行
  warningLogger({
    message: "This is a warning message",
    metadata: { id: "ab85592d-b7cf-4623-9884-aa70f40814f7" },
  });
};

initializedSomeService();

结果:

TYPE:warning {
  data: {
    message: 'This is a warning message',
    metadata: { id: 'ab85592d-b7cf-4623-9884-aa70f40814f7' }
  }
}

何时应该使用调用签名?

当你需要以下特性的东西时使用它们:

  • ✅ 像函数一样可调用
  • ✅ 可通过属性进行配置
  • ✅ 强类型

典型的实际使用场景:

  • 日志工具
  • 分析追踪器
  • 功能标志执行器
  • SDK 风格的 API(例如,像 dayjs 这样的库)

闭包 vs 调用签名(快速洞察)

闭包调用签名
状态处理封装状态暴露状态
可变性防止变异更安全可变
风格更函数式更库式
常见模式Hook(钩子)SDK 设计

两者各有优劣——根据你的设计目标选择。

  • 如果你想要不可变性 → 更倾向于使用闭包。
  • 如果你想要可配置性 → 调用签名更出色。

最后思考

调用签名是 TypeScript 的一个小特性,但一旦你理解了它们,你会在许多库中开始注意到这种模式。它们提供了一种强大的方式来设计符合 JavaScript 自然感觉的 API —— 可配置、可调用且强类型。

绝对值得加入你的 TypeScript 工具箱 🚀

Back to Blog

相关文章

阅读更多 »

TypeScript 或 泪水

前端质量门 另请参见:后端质量门 后端 linters 捕获 async footguns。Type checkers 防止 runtime explosions。现在轮到前端的……

Deno 沙盒

抱歉,我无法直接访问外部链接。请提供您想要翻译的具体摘录或摘要文本,我会为您翻译成简体中文。