Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~26 min· topic 7 of 7

Topic 2.7

Call Signatures and Function Objects

In one line

In JavaScript a function is also an object that can carry properties, and a class can be called with new. TypeScript describes both with call signatures and construct signatures inside object types.

Think of it like this

A TV remote. You can point it and press a button (use it as a function), but it also has a label and a battery you can inspect (properties). A function object is a remote: you can call it, and it also has things attached to it. And a factory door marked "enter here to get a new toy" is a construct signature: you don't just call it, you ask it to build something with new.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Call signature
A member of an object type, (x: number): string, that says the value can be called.
Construct signature
A member written new (x: number): Thing, that says the value can be called with new to build something.
Function object
A function seen as an object that may have properties attached.
Expando property
A property you add to a function after declaring it, like slugify.calls = 0. TypeScript tracks it.
Constructor
The special function of a class that creates an instance when called with new.
Instance
An object created from a class with new.
Function (type)
A built-in type meaning "any callable". It turns off argument and result checking.
Intersection type
A type written A & B that has everything from both.

Step by step

01A function that also has properties

Put the call signature and the members in one object type. makeCounter returns something you can call (counter()) and also read (counter.count) and reset (counter.reset()).

counter.tswhole filets
type Counter = {
  (): number;
  reset(): void;
  count: number;
};

02Build one with a cast, then fill it in

A plain arrow function doesn't have reset or count yet, so TypeScript can't prove it fits Counter. Here we use a type assertion (as Counter: "trust me, this has that type", taught in Topic 5.6), add the properties, and return it.

The asserted object is only safe because the next lines really add them.

counter.tswhole filets
function makeCounter(): Counter {
  const counter = (() => ++counter.count) as Counter;
  counter.count = 0;
  counter.reset = () => { counter.count = 0; };
  return counter;
}

03Construct signatures: functions you call with new

new (x: number, y: number) => { x: number; y: number } is the type of anything that builds such a point when called with new. A class fits it.

Without the new the call is rejected, because the type has no call signature.

maker.tswhole filets
type PointMaker = new (x: number, y: number) => { x: number; y: number };

function build(Maker: PointMaker) {
  return new Maker(1, 2);
}

04Why not just use Function

Function accepts any call and returns any. The checker can't tell you that you passed three arguments to a one-argument function or that the result is a string.

Write the shape you expect instead. It costs one line and keeps every check.

Why not just use Functiondiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a second call signature

    In the first example add (start: number): number; to Counter. Predict whether tsc complains about makeCounter. It does not: as Counter told TypeScript to trust you, so the second signature is promised but never built. Call next(10) and notice it is accepted while the real function ignores the 10. A cast can't check a promise.

  2. 2

    Write it as a function type

    Try replacing type Counter by type Counter = () => number. Predict which lines turn red. The members reset and count no longer exist on the type.

  3. 3

    Call a wrong shape

    In the last example, change Function to (...args: number[]) => unknown and re-run. The call that passes (a: string) => ... is now rejected, and the result of fn(1, 2, 3) is unknown instead of any. Compare with the version using Function.

Code & diagrams

A callable object with properties

next is called like a function and read like an object. The last line assigns text to a number property. Note the unsafe step: as Counter tells TypeScript to trust you until the members are filled in.

Console

(nothing was printed)

Expected console output

1 2 2
0
Properties on functions: expando and Object.assign

slugify.calls = 0 is accepted without declaring it: TypeScript saw the assignment next to the function. Object.assign gives an intersection: a function type plus an object with label.

Console

(nothing was printed)

Expected console output

hello-world number
2 inc
Construct signatures

Point fits PointMaker. A class whose instances lack x and y does not. The last line calls a construct-only type without new.

Console

(nothing was printed)

Expected console output

Point { x: 1, y: 2 }
Function versus a specific function type

Function accepted a function that wants a string, called it with numbers, and returned any, so the bug only shows at runtime. The typed version rejects the wrong callback. The last lines show what is not callable.

Console

(nothing was printed)

Expected console output

Uncaught TypeError: a.toUpperCase is not a function

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call a construct signature without new

Declare type PointMaker = new (x: number, y: number) => { x: number; y: number } and write Maker(1, 2).

terminal
$ Types panel
── what you'll see ──
index.ts(10,37): error TS2348: Value of type 'PointMaker' is not callable. Did you mean to include 'new'?

Break #2

Assign the wrong type to a function's property

With the Counter type, write next.count = "zero";.

terminal
$ Types panel
── what you'll see ──
index.ts(18,1): error TS2322: Type 'string' is not assignable to type 'number'.

Myth vs fact

Myth

A function can't have properties in JavaScript or TypeScript.

Fact

In JavaScript a function is an object, so it can. TypeScript describes that with a call signature in an object type, or tracks properties added right after a function declaration.

Myth

Function is a good type for "any function".

Fact

It disables argument and result checking (the result is any). Use a specific signature, or (...args: never[]) => unknown for "any function".

Myth

A class can be called like a function.

Fact

No: calling it without new throws at runtime. TypeScript's construct signature (new (...) => T) is how you require new.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Call signatures and function type expressions are interchangeable for a type with one signature, except that the arrow form can't hold members. In interfaces the call signature form is the only way (Topic 3.2). When a type has several call signatures, the first matching one is used, like overloads (Topic 2.5).

  • ▸

    Expando properties are recognised only for function declarations and const function expressions or arrows, and only when the assignment sits in the same scope as the declaration. An assignment from elsewhere is an ordinary error (TS2339, the property does not exist). For a shape you want to state up front, write the object type with a call signature, or merge a namespace into the function (Topic 16.3).

  • ▸

    typeof SomeClass is the type of the class constructor, which includes its static members and its construct signature, while SomeClass as a type means an instance. InstanceType<typeof Point> and ConstructorParameters<typeof Point> read the construct signature back out (Topic 10.2).

  • ▸

    Function also exists as the apparent type of every function for members like .call, .bind and .length. It is why strictBindCallApply has a separate CallableFunction interface in lib.es5.d.ts: so those members get precise types instead of the loose ones on Function.

Remember this

  1. 1

    The problem this solves: Functions in JavaScript can carry properties, and can be called with new to build objects. A plain function type can't describe either, so those uses go unchecked. In JavaScript a function is an object with a special ability: it can be called (typeof reports "function", but it is still an object). Because it is an object, you can hang properties on it (slugify.calls = 0), and libraries do this all the time (axios is a function and also has axios.get; jQuery is a function and has $.ajax). To describe a value like that you need a type that says both "callable" and "has these properties".

  2. 2

    A call signature inside an object type does that. It looks like a method without a name: type Counter = { (): number; reset(): void; count: number }. The (): number part says "you can call it with no arguments and get a number"; the rest are ordinary members. Parameters go inside the parentheses as usual, and a type can have several call signatures (an overloaded function, Topic 2.5).

  3. 3

    Two spellings describe the same function type: the short function type expression (n: number) => string (Topic 2.1) and the call signature { (n: number): string }. Notice the arrow => in one and the colon : in the other. Use the short form when it is only a function. Use the call signature form when you need properties or several signatures in the same type.

  4. 4

    TypeScript can also work out the properties you add to a function. If you declare function slugify(...) or const f = () => ... and then assign slugify.calls = 0 in the same scope, TypeScript treats it as a declared property (an expando property). For more control, Object.assign(fn, { label: "inc" }) returns the intersection ((n: number) => number) & { label: string } (Topic 3.3), which you can name with a type.

  5. 5

    A construct signature describes something you can call with new: new (x: number, y: number) => Point. It looks like a call signature with new in front. A class constructor has this type. If you want a parameter that accepts "a class that builds a Point" (a factory, dependency injection, a registry of classes), type it like Maker: new (x: number, y: number) => { x: number; y: number }. Passing the class Point is accepted if its constructor and instances fit. Calling a new-only value without new is TS2348: Value of type 'PointMaker' is not callable. Did you mean to include 'new'?.

  6. 6

    What about the built-in type **Function**? It means "anything callable" and nothing more. A value typed Function can be called with any arguments and returns any, so every check is off: fn(1, 2, 3) is accepted even if the real function wants a string. It is almost never what you want, and linters ban it. Prefer a specific function type such as (n: number) => number, or (...args: never[]) => unknown if you truly need "any function".

  7. 7

    Rule of thumb: describe a plain callback with a function type expression. Add a call signature only when the value is callable and has members, or has several signatures. Add a construct signature when something must be created with new. And reach for Function or any for functions never: they remove exactly the checks you came here for.

Explain it without notes

01

What is a call signature and when do you need one instead of (x) => y?

02

What is a construct signature?

03

Why is the Function type discouraged?

04

How does TypeScript handle properties added to a function?

Practice

01

Write a type Logger that is callable with a string (returns void) and has a level: "info" | "warn" property. Create one with an arrow function and Object.assign, call it, and read level.

02

Write function create(Maker: new (name: string) => { name: string }, name: string) that returns new Maker(name). Pass a class User with a constructor taking a name. Which call gives an error: passing a class with a numeric constructor parameter?

03

Replace the Function type in function call(fn: Function) { return fn(); } with a type that only accepts functions taking no arguments and returning a string.

Trade-offs

  • ↔

    Attaching properties to functions is compact (one export that is both callable and a namespace), but it hides state in a global-ish object and is harder to type and to tree-shake. Often a plain object with methods, or a class, is clearer.

  • ↔

    Construct signatures make factories flexible, but classes with different constructors rarely fit one shape. Keep the signature small, or accept a factory function (() => T) instead of a class.

Done when you can

  • I can write an object type with a call signature and members.

  • I can say when to use a function type expression and when a call signature.

  • I can write a construct signature and explain TS2348.

  • I can explain why Function is unsafe and what to use instead.

  • I can describe how properties on functions are typed.