TypeScriptCall & Construct Signatures

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:

TS
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:

TS
// 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:

TS
// 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"
Note
The implementation assigns the function to a variable typed as 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:

TS
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:

TS
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:

TS
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 string
Warning
The variable fmt 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:

TS
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);      // 2
Practical 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:

TS
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 runtime
Construct 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:

TS
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, '');
Success
Typed construct signatures let you write truly polymorphic factory functions. TypeScript guarantees at compile time that any class you pass in has the right constructor signature — no runtime surprises.
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