Functions

TypeScript · Lesson 6 of 28 6 min read Updated

Functions are where TypeScript pays off the most. A typed function tells every caller exactly what to pass in and what comes back, and the compiler checks every call in your whole project. If you later change the function, TypeScript shows you every place that needs updating.

In this lesson you will type parameters and return values, use optional, default and rest parameters, describe functions as types for callbacks, and finish with function overloads.

Parameter and return types

Annotate each parameter with a type. You can also annotate the return type after the parameter list. If you leave the return type out, TypeScript infers it from the return statements.

TypeScriptRun it yourself
function gstAmount(price: number, ratePercent: number): number {
  return Math.round((price * ratePercent) / 100);
}

function formatRupees(amount: number): string {
  return "₹" + amount.toLocaleString("en-IN");
}

function printReceipt(item: string, price: number): void {
  const gst = gstAmount(price, 18);
  console.log(`${item}: ${formatRupees(price)} + GST ${formatRupees(gst)}`);
}

printReceipt("Laptop bag", 1299);
printReceipt("Office chair", 15499);
Output
Laptop bag: ₹1,299 + GST ₹234
Office chair: ₹15,499 + GST ₹2,790

void means the function does not return a useful value. Writing return types is optional, but it is a good habit for exported functions: the compiler then checks the function body against your intention, and readers see the result type without reading the code.

The compiler checks every call and every return:

TypeScript
function average(marks: number[]): number {
  if (marks.length === 0) {
    return "no marks";
  }
  return marks.reduce((a, b) => a + b, 0) / marks.length;
}

average([80, 90], 2);
average();
Output
app.ts(3,5): error TS2322: Type 'string' is not assignable to type 'number'.
app.ts(8,19): error TS2554: Expected 1 arguments, but got 2.
app.ts(9,1): error TS2554: Expected 1 arguments, but got 0.

Unlike JavaScript, TypeScript does not let you pass too many or too few arguments.

Optional, default and rest parameters

A parameter followed by ? is optional. Inside the function its type includes undefined, so you must handle the missing case. A default value makes a parameter optional too, and its type is inferred from the default. A rest parameter collects any number of arguments into an array.

TypeScriptRun it yourself
function greet(name: string, title?: string): string {
  if (title) {
    return `Namaste, ${title} ${name}`;
  }
  return `Namaste, ${name}`;
}

function ticketPrice(base: number, quota = "general"): number {
  return quota === "tatkal" ? base + 400 : base;
}

function totalRuns(batter: string, ...scores: number[]): string {
  const sum = scores.reduce((a, b) => a + b, 0);
  return `${batter}: ${sum} runs in ${scores.length} innings`;
}

console.log(greet("Meera"));
console.log(greet("Sharma", "Dr."));
console.log(ticketPrice(1200), ticketPrice(1200, "tatkal"));
console.log(totalRuns("Rohan", 45, 12, 88, 0));
Output
Namaste, Meera
Namaste, Dr. Sharma
1200 1600
Rohan: 145 runs in 4 innings

Optional parameters must come after the required ones. The parameter quota has the inferred type string because its default is a string.

Function types and callbacks

Functions are values in JavaScript, so they need types too. A function type looks like an arrow function without a body: (price: number) => number means "a function that takes a number and returns a number". Give it a name with type when you use it more than once.

TypeScriptRun it yourself
type PriceRule = (price: number) => number;

const festiveSale: PriceRule = (price) => price * 0.8;
const flatOff: PriceRule = (price) => Math.max(price - 200, 0);
const noDiscount: PriceRule = (price) => price;

function applyRule(prices: number[], rule: PriceRule): number[] {
  return prices.map(rule);
}

const cart = [999, 450, 1500];
console.log(applyRule(cart, festiveSale));
console.log(applyRule(cart, flatOff));
console.log(applyRule(cart, noDiscount));

const names = ["kabir", "isha", "farhan"];
console.log(names.map((n) => n.charAt(0).toUpperCase() + n.slice(1)));
Output
[ 799.2, 360, 1200 ]
[ 799, 250, 1300 ]
[ 999, 450, 1500 ]
[ 'Kabir', 'Isha', 'Farhan' ]

Notice that (price) => price * 0.8 has no annotation on price. TypeScript already knows it must be a number from the PriceRule type. This is called contextual typing: the place where a function is used tells TypeScript its parameter types. The same thing happens in the last line, where n is known to be a string because names is a string[].

A callback type with a void return accepts functions that return something, which is why you can pass (x) => list.push(x) to forEach. The returned value is simply ignored.

Function overloads

Sometimes one function accepts different kinds of input and returns different types for each. Overloads let you write several signatures for one function. You list the signatures callers can use, then write one implementation that handles all of them.

TypeScriptRun it yourself
function parseMarks(input: string): number[];
function parseMarks(input: number[]): string;
function parseMarks(input: string | number[]): number[] | string {
  if (typeof input === "string") {
    return input.split(",").map((part) => Number(part.trim()));
  }
  return input.join(", ");
}

const list = parseMarks("78, 91, 64"); // type: number[]
const text = parseMarks([55, 82]);     // type: string

console.log(list, list.length);
console.log(text.toUpperCase());
Output
[ 78, 91, 64 ] 3
55, 82

Callers see only the first two signatures, so parseMarks("1,2") is known to return number[] and parseMarks([1, 2]) to return string. The third signature belongs to the implementation and is hidden from callers.

Common mistakes

  • Leaving parameters without types. In strict mode this is error TS7006: Parameter 'x' implicitly has an 'any' type. Annotate every parameter of a normal function.
  • Putting an optional parameter first. function f(a?: string, b: number) is an error; required parameters come first.
  • Using an optional parameter without checking it. title.toUpperCase() fails when title is undefined. Check it first or give it a default.
  • Annotating callback parameters that are already known. In names.map((n: string) => ...) the annotation is redundant; contextual typing does it.

Key points

  • Annotate parameters; return types are inferred but are worth writing on exported functions.
  • void marks a function with no useful return value.
  • param? is optional, param = value has a default, and ...rest: T[] collects extra arguments.
  • Function types such as (price: number) => number describe callbacks, and contextual typing fills in callback parameter types.
  • Overloads give one function several typed signatures; prefer unions or generics when they are enough.

Exercise

Write a type Validator = (value: string) => string | null that returns an error message or null. Create two validators: notEmpty and isPincode (exactly 6 digits). Then write validate(value: string, ...rules: Validator[]): string[] that returns all error messages. Test it with "", "1100" and "110001".

Show solution
TypeScript
type Validator = (value: string) => string | null;

const notEmpty: Validator = (value) => (value.trim() === "" ? "Required" : null);
const isPincode: Validator = (value) => (/^\d{6}$/.test(value) ? null : "Pincode must be 6 digits");

function validate(value: string, ...rules: Validator[]): string[] {
  const errors: string[] = [];
  for (const rule of rules) {
    const message = rule(value);
    if (message !== null) errors.push(message);
  }
  return errors;
}

console.log(validate("", notEmpty, isPincode));       // [ 'Required', 'Pincode must be 6 digits' ]
console.log(validate("1100", notEmpty, isPincode));   // [ 'Pincode must be 6 digits' ]
console.log(validate("110001", notEmpty, isPincode)); // []

The rest parameter accepts any number of validators, and each validator's value parameter gets its type from the Validator function type.

Quick Quiz

1. What does the return type void mean?

2. Which declaration is valid?

3. In const rule: PriceRule = (price) => price * 0.8;, how does TypeScript know price is a number?

4. How many signatures of an overloaded function can callers see?

Learn TypeScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call