Grokking TypeScript
Vote

0% completed

​

Call Signatures

Naming a Function Type

Passing a Function as an Argument

Call Signatures

Function Types in Interfaces

Function Types with a Type Parameter

Choosing a Form

A first-class function is a function that a program can treat like any other value: a variable can hold it, an argument can carry it, and another function can return it. JavaScript functions are first-class, so TypeScript needs a way to describe a function without writing its body.

TypeScript has two ways to write that description. The (a: number) => number form from the previous lesson is called a function type expression, and it covers most cases. The second form is called a call signature, and it can describe something the first form cannot: a function that also has properties. This lesson uses a shop's price rules to show both.

Naming a Function Type

A price rule takes a price and returns a new price. Writing that function type on every variable would repeat it, so we give it a name with a type alias.

type PriceRule = (price: number) => number; const halfOff: PriceRule = (price) => price / 2; const fixedFee: PriceRule = () => 5; console.log(halfOff(100)); // Output: 50 console.log(fixedFee(100)); // Output: 5

PriceRule is a compile-time name only, and nothing about it exists when the program runs. The compiler uses it to give price the type number and to check each returned value. fixedFee takes no parameters, and that is allowed because a function may ignore the arguments it receives.

If a rule returns the wrong type, the compiler reports the returned value:

type PriceRule = (price: number) => number; const badRule: PriceRule = (price) => `${price} USD`; // Error
error TS2322: Type 'string' is not assignable to type 'number'.

Passing a Function as an Argument

A function that takes another function as an argument, or returns a function, is called a higher-order function. A named function type keeps its signature short.

type PriceRule = (price: number) => number; function applyRule(price: number, rule: PriceRule): number { return rule(price); } const halfOff: PriceRule = (price) => price / 2; console.log(applyRule(100, halfOff)); // Output: 50 console.log(applyRule(100, (price) => price - 15)); // Output: 85

Follow the second call in the picture below. The argument (price) => price - 15 has no types of its own, but the compiler gets them from the rule parameter. It does this before anything runs, and at runtime applyRule simply calls whichever function it received.

applyRule receives a price and a rule function, calls the rule with the price, and returns the result.
applyRule receives a price and a rule function, calls the rule with the price, and returns the result.

An argument that breaks the rule is reported at the call:

type PriceRule = (price: number) => number; function applyRule(price: number, rule: PriceRule): number { return rule(price); } applyRule(100, (price) => "free"); // Error
error TS2322: Type 'string' is not assignable to type 'number'.

Call Signatures

Suppose the receipt must show a label beside each rule, so a rule must be callable and must also have a label property. A function type expression cannot list a property, because it describes only parameters and a return value. A call signature can, because it is written inside an object type.

type LabeledRule = { label: string; (price: number): number; }; function halfPrice(price: number): number { return price / 2; } halfPrice.label = "Half off"; function applyLabeledRule(price: number, rule: LabeledRule): string { return `${rule.label}: ${rule(price)}`; } console.log(applyLabeledRule(100, halfPrice)); // Output: Half off: 50

The line (price: number): number; is the call signature. It has no name, which tells the compiler that the object itself can be called. It also uses a colon before the return type, where a function type expression uses =>.

A function declaration may receive properties by assignment, as halfPrice.label does here, and the compiler adds them to the type of the function. A function without the property does not match:

type LabeledRule = { label: string; (price: number): number; }; function plainRule(price: number): number { return price; } const rule: LabeledRule = plainRule; // Error
error TS2741: Property 'label' is missing in type '(price: number) => number' but required in type 'LabeledRule'.

Function Types in Interfaces

An interface is a named description of the shape of an object: its properties and its methods. The next chapter covers interfaces in depth, and this definition is enough for now. An interface can include functions in the three ways shown below.

interface Pricing { discount: (price: number) => number; // a property that holds a function tax(price: number): number; // a method } const pricing: Pricing = { discount: (price) => price - 10, tax: (price) => price * 1.5, }; console.log(pricing.discount(100)); // Output: 90 console.log(pricing.tax(100)); // Output: 150 interface LabeledRule { (price: number): number; // a call signature label: string; }

The members of Pricing are not call signatures, because each one has a name. You call them through the object, as pricing.discount(100). LabeledRule does have a call signature, so you would call a value of that type directly. It means the same as the object type in the earlier section.

The compiler checks each function in the object against the interface, in the same way as a variable with a function type:

interface Pricing { discount: (price: number) => number; tax(price: number): number; } const pricing: Pricing = { discount: (price) => "ten off", // Error tax: (price) => price, };
error TS2322: Type 'string' is not assignable to type 'number'.

Function Types with a Type Parameter

A type parameter is a placeholder for a type, written in angle brackets, and a later chapter on generics explains it in full. It can appear in a function type in two positions, and the position decides when the placeholder gets its real type.

type IdentityFunction<T> = (value: T) => T; const keepNumber: IdentityFunction<number> = (value) => value; console.log(keepNumber(10)); // Output: 10 keepNumber("ten"); // Error
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Here T belongs to the alias, so it is fixed when the alias is used. IdentityFunction<number> is an ordinary function type that accepts only numbers, and it does not accept several types. The next example moves T to the signature itself, which gives a generic signature.

type GenericIdentity = <T>(value: T) => T; const keepAny: GenericIdentity = (value) => value; console.log(keepAny(10)); // Output: 10 console.log(keepAny("ten")); // Output: ten

Now the compiler chooses T again at each call, so the first call returns a number and the second returns a string.

Choosing a Form

Use the form that says what you need and no more:

  • Write the function type directly on a variable or parameter when it appears only once.
  • Give it a name with a type alias when several functions must share it, as the price rules do.
  • Write a call signature when the function also needs properties.

Reading Progress

0%


Vote for new content

On This Page

Naming a Function Type

Passing a Function as an Argument

Call Signatures

Function Types in Interfaces

Function Types with a Type Parameter

Choosing a Form