Call & Construct Signatures
In JavaScript, functions are objects. You can call them, but you can also attach
properties to them — fn.displayName = 'myFunc' is perfectly valid. TypeScript
needs a way to describe this: a value that is both callable and has properties.
That is what call signatures and construct signatures are for.
Two Ways to Write a Function Type
You already know the shorthand function type expression:
type Greeter = (name: string) => string;
This is fine for simple cases. But it cannot describe a function that also has properties. The call signature syntax writes the same thing inside an object type or interface:
// Call signature inside a type alias
type Greeter = {
(name: string): string;
};
// Call signature inside an interface
interface Greeter {
(name: string): string;
}Both forms describe exactly the same callable type. The key difference is the syntax: call signatures use a colon before the return type, not an arrow.
Form | Syntax | Use case |
|---|---|---|
Function type expression | (x: number) => string | Simple callbacks, no extra properties |
Call signature in type alias | { (x: number): string } | Function with additional properties |
Call signature in interface | interface F { (x: number): string } | Extendable, declarable merging |
Why You Need Call Signatures
The moment you want to add a property to a callable value, the shorthand arrow syntax breaks down:
// This type says: "a function that takes a string and returns void"
// but it cannot have any extra properties
type SimpleLog = (msg: string) => void;
// What if our logger also has a prefix and a count property?
// We need a call signature:
interface Logger {
(msg: string): void;
prefix: string;
callCount: number;
}
// Now we can implement it:
function createLogger(prefix: string): Logger {
let callCount = 0;
const log: Logger = (msg: string) => {
callCount++;
log.callCount = callCount;
console.log(`[${log.prefix}] ${msg}`);
};
log.prefix = prefix;
log.callCount = 0;
return log;
}
const warn = createLogger('WARN');
warn('disk space low');
warn('memory usage high');
console.log(warn.callCount); // 2
console.log(warn.prefix); // "WARN"Logger, then assigns properties to that variable. TypeScript checks that all required properties are present before the function is returned.Construct Signatures
A construct signature describes a value that can be called with new. You
write it the same way as a call signature but with the new keyword in front:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log('beep beep');
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log('tick tock');
}
}
// A factory function that accepts any clock constructor
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
const digital = createClock(DigitalClock, 12, 17);
const analog = createClock(AnalogClock, 7, 32);The parameter ctor is typed as ClockConstructor — anything with a
new (h, m) signature that returns a ClockInterface. TypeScript verifies at
the call site that you pass a compatible class.
Combining Call and Construct Signatures
Some values in JavaScript can be called both ways — with and without new. The
classic example is Date: Date() returns a string, new Date() returns a
Date object. You can model this with both signatures in one interface:
interface DateLike {
// Callable without new → returns a string
(): string;
// Callable with new → returns a Date
new (): Date;
new (value: number | string): Date;
new (
year: number,
month: number,
date?: number,
hours?: number,
minutes?: number,
seconds?: number,
ms?: number
): Date;
}Overload Signatures in Interfaces
A call signature inside an interface can appear multiple times to describe overloaded behaviour — the same function called with different argument shapes:
interface Formatter {
(value: string): string;
(value: number, decimals: number): string;
(value: Date, format: string): string;
}
// Implementation must handle all three variants
const fmt: Formatter = (
value: string | number | Date,
secondArg?: number | string
): string => {
if (typeof value === 'string') return value.trim();
if (typeof value === 'number') return value.toFixed(secondArg as number ?? 2);
return (value as Date).toLocaleDateString(undefined, { dateStyle: 'short' });
};
fmt(' hello '); // "hello"
fmt(3.14159, 2); // "3.14"
fmt(new Date(), 'short'); // locale date stringfmt is typed as Formatter, but the arrow function assigned to it must use union types in its implementation signature. TypeScript does not automatically widen the parameter types for you.Hybrid Types — Callable and Indexable
You can combine a call signature with index signatures to create a type that is both callable and behaves like a dictionary:
interface StringCache {
(key: string): string | undefined;
[key: string]: string | undefined;
size: number;
}
function createCache(): StringCache {
const store: Record<string, string> = {};
let size = 0;
const cache = ((key: string) => store[key]) as StringCache;
cache.size = 0;
// Wrap with a Proxy to intercept property sets and update size
return new Proxy(cache, {
set(target, prop, value) {
if (typeof prop === 'string' && prop !== 'size') {
if (!(prop in target)) {
target.size++;
}
}
(target as Record<string, unknown>)[prop as string] = value;
return true;
},
});
}
const cache = createCache();
cache['user:1'] = 'Alice';
cache['user:2'] = 'Bob';
console.log(cache('user:1')); // "Alice"
console.log(cache.size); // 2Practical Example: Middleware Pipeline
A real-world pattern is a middleware function that is also configurable. The call signature describes what the middleware does; the properties describe its configuration:
type Request = { url: string; method: string; body?: unknown };
type Response = { status: number; body: unknown };
type Next = () => void;
interface Middleware {
(req: Request, res: Response, next: Next): void;
// Configuration properties
name: string;
enabled: boolean;
}
function cors(origins: string[]): Middleware {
const handler: Middleware = (req, res, next) => {
if (!handler.enabled) return next();
const origin = (req as Record<string, string>).origin ?? '';
if (origins.includes(origin)) {
(res as Record<string, unknown>)['Access-Control-Allow-Origin'] = origin;
}
next();
};
handler.name = 'cors';
handler.enabled = true;
return handler;
}
const corsMiddleware = cors(['https://example.com']);
console.log(corsMiddleware.name); // "cors"
corsMiddleware.enabled = false; // disable at runtimeConstruct Signature for Abstract Factory
The construct signature shines in dependency-injection and factory patterns where you want to accept any class that satisfies a contract:
interface Repository<T> {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<void>;
}
// A construct signature for any Repository implementation
interface RepositoryClass<T> {
new (connectionString: string): Repository<T>;
}
interface User {
id: string;
name: string;
}
class PostgresUserRepo implements Repository<User> {
constructor(private conn: string) {}
async findById(id: string) { return null; }
async save(user: User) {}
}
class InMemoryUserRepo implements Repository<User> {
private store = new Map<string, User>();
constructor(_conn: string) {}
async findById(id: string) { return this.store.get(id) ?? null; }
async save(user: User) { this.store.set(user.id, user); }
}
function buildRepo<T>(
RepoCls: RepositoryClass<T>,
connectionString: string
): Repository<T> {
return new RepoCls(connectionString);
}
const prod = buildRepo(PostgresUserRepo, 'postgres://...');
const test = buildRepo(InMemoryUserRepo, '');Common Mistakes
Using an arrow type (x: T) => U when you need properties on the callable — switch to a call signature
Forgetting that the implementation variable must be cast (as InterfaceName) before properties are assigned
Mixing call signature and arrow-type syntax in the same interface — stick to one style
Assuming construct signatures work on arrow functions — they do not, only regular functions and classes
Exposing an overload's implementation signature to callers — only the overload signatures are visible
Quick Reference
Call signature: { (param: Type): ReturnType } — in a type alias or interface
Construct signature: { new (param: Type): ReturnType }
Overload signatures: list multiple call signatures in one interface
Hybrid types: combine call signature + properties + index signatures
Arrow type (x: T) => U — shorthand, cannot have properties
Use call signatures when a function value also carries metadata or methods