Build a simple Mini Day.js Clone & Logger Middleware by Call Signatures in TypeScript
Source: Dev.to
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.
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 in libraries like dayjs or when implementing a logger middleware. 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.
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.
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
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();
Result:
TYPE:warning {
data: {
message: 'This is a warning message',
metadata: { id: 'ab85592d-b7cf-4623-9884-aa70f40814f7' }
}
}
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 (e.g., libraries like dayjs)
Closure vs Call Signature (Quick Insight)
| Closure | Call Signature | |
|---|---|---|
| State handling | Encapsulates state | Exposes state |
| Mutability | Safer from mutation | Mutable |
| Style | More functional | More library‑style |
| Common patterns | Hooks | 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 🚀