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 withnewto 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 & Bthat 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()).
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.
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.
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.
Try it yourself
- 1
Add a second call signature
In the first example add
(start: number): number;toCounter. Predict whether tsc complains aboutmakeCounter. It does not:as Countertold TypeScript to trust you, so the second signature is promised but never built. Callnext(10)and notice it is accepted while the real function ignores the 10. A cast can't check a promise. - 2
Write it as a function type
Try replacing
type Counterbytype Counter = () => number. Predict which lines turn red. The membersresetandcountno longer exist on the type. - 3
Call a wrong shape
In the last example, change
Functionto(...args: number[]) => unknownand re-run. The call that passes(a: string) => ...is now rejected, and the result offn(1, 2, 3)isunknowninstead ofany. Compare with the version usingFunction.
Code & diagrams
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
0slugify.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 incPoint 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 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 functionBreak 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).
Break #2
Assign the wrong type to a function's property
With the Counter type, write next.count = "zero";.
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
functiondeclarations andconstfunction 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 anamespaceinto the function (Topic 16.3). - ▸
typeof SomeClassis the type of the class constructor, which includes its static members and its construct signature, whileSomeClassas a type means an instance.InstanceType<typeof Point>andConstructorParameters<typeof Point>read the construct signature back out (Topic 10.2). - ▸
Functionalso exists as the apparent type of every function for members like.call,.bindand.length. It is whystrictBindCallApplyhas a separateCallableFunctioninterface inlib.es5.d.ts: so those members get precise types instead of the loose ones onFunction.
Remember this
- 1
The problem this solves: Functions in JavaScript can carry properties, and can be called with
newto 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 (typeofreports"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 (axiosis a function and also hasaxios.get;jQueryis a function and has$.ajax). To describe a value like that you need a type that says both "callable" and "has these properties". - 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(): numberpart 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
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
TypeScript can also work out the properties you add to a function. If you declare
function slugify(...)orconst f = () => ...and then assignslugify.calls = 0in 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
A construct signature describes something you can call with
new:new (x: number, y: number) => Point. It looks like a call signature withnewin 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 likeMaker: new (x: number, y: number) => { x: number; y: number }. Passing the classPointis accepted if its constructor and instances fit. Calling anew-only value withoutnewisTS2348: Value of type 'PointMaker' is not callable. Did you mean to include 'new'?. - 6
What about the built-in type **
Function**? It means "anything callable" and nothing more. A value typedFunctioncan be called with any arguments and returnsany, 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[]) => unknownif you truly need "any function". - 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 forFunctionoranyfor functions never: they remove exactly the checks you came here for.
Explain it without notes
What is a call signature and when do you need one instead of (x) => y?
What is a construct signature?
Why is the Function type discouraged?
How does TypeScript handle properties added to a function?
Practice
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.
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?
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
Functionis unsafe and what to use instead.I can describe how properties on functions are typed.