DEV Community

Cover image for Build a simple Mini Day.js Clone & Logger Middleware by Call Signatures in TypeScript
Kelvyn Thai
Kelvyn Thai

Posted on

Build a simple Mini Day.js Clone & Logger Middleware by Call Signatures in TypeScript

Hi everyone,

Yesterday I spent some time learning more about TypeScript, and I came
across the term "call signature." It's quite helpful when you want
to define something like a logger middleware or understand how libraries
such as dayjs declare their typings.
Source: https://www.typescriptlang.org/docs/handbook/2/functions.html#call-signatures


First, understand that a function is an object

You should know that the typeof a function prototype is object ---
I'm referring to the prototype, not typeof function itself.

If you are not familiar with prototypes, I strongly suggest reading
about them first before continuing (https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes).


The problem we want to solve

How can we configure a function and then execute it with those
configurations?

This is somewhat similar to a closure, but closures encapsulate
state using variable scope. With a call signature, the state is exposed
outside --- meaning we can mutate the properties if needed.

You might encounter this pattern somewhere in your codebase, especially
in libraries like dayjs or when implementing a logger middleware.

But how do we define it properly in TypeScript?


Call Signatures in TypeScript

In JavaScript, functions can have properties in addition to being
callable.

However, the typical function type syntax does not allow declaring
properties
. If we want to describe something callable with
properties
, we can use a call signature inside an object type.

Before continuing, it's helpful if you already understand closures or
have used them to encapsulate function state (for example, in React
hooks).

A call signature is similar to a closure --- but the key difference is
that the state lives outside the function, so it can be modified.

Let's look at an example.


Example 1 --- Mini Day.js Clone

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
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Here, dayjs is both:

  • callable like a function\
  • configurable via properties

This is exactly what a call signature enables.


Example 2 --- Logger Middleware

Now let's implement a logger middleware with configurable properties
such as:

  • isEnabled (based on production env)
  • log type (warning, error, info)
  • message payload
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 });
  };

  // configure properties
  warningLogger.isEnabled = process.env.NODE_ENV === "production";
  warningLogger.type = "warning";

  // execute like a function
  warningLogger({
    message: "This is a warning message",
    metadata: { id: "ab85592d-b7cf-4623-9884-aa70f40814f7" },
  });
};

initializedSomeService();
Enter fullscreen mode Exit fullscreen mode

Result:

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

When should you use call signatures?

Use them when you want something that is:

βœ… callable like a function\
βœ… configurable via properties\
βœ… strongly typed

Typical real-world use cases:

  • logger utilities\
  • analytics trackers\
  • feature flag executors\
  • SDK-style APIs\
  • libraries like dayjs

Closure vs Call Signature (Quick Insight)

Closure Call Signature


Encapsulates state Exposes state
Safer from mutation Mutable
More functional style More library-style
Common in hooks Common in SDK design

Neither is better --- choose based on your design goal.

If you want immutability β†’ prefer closures.\
If you want configurability β†’ call signatures shine.


Final Thoughts

Call signatures are a small TypeScript feature, but once you understand
them, you'll start noticing this pattern in many libraries.

They provide a powerful way to design APIs that feel natural to
JavaScript --- configurable, callable, and strongly typed.

Definitely worth adding to your TypeScript toolbox πŸš€

Top comments (0)