Functions and Type Annotations

Learn functions and type annotations in Typescript in this lesson!

We'll cover the following

Functions also get to participate in the static typing fun. The parameters and return values can have type annotations, and the function as a whole has its own static type.

Function parameters

TypeScript function parameters can have type annotations, with a similar syntax to what we’ve seen for variable declarations. The annotations work whether the function is a named function, an anonymous function, or a method of a class. All three of these examples have the same typing:

function priceOfTicket(row: number, accessType: string) : number { }

let priceOfTicket = function(row: number, accessType: string) : number { }

class Ticket {
  priceOfTicket(row: number, accessType: string) : number { }
}

In all three cases, the function priceOfTicket expects two arguments: the first a number, the second a string, and returns a number.

Let’s talk about the return type first. As currently written, all three of these functions would fail compilation because they claim to return a number, but at the moment they don’t return anything. The TypeScript compiler will not compile a function that sets a return type but does not return a value.

If you want to claim explicitly that the function will not return a value, then you can use the special type void, which means “no value”:

function sendAnAlert(message: string) : void { }

Now you get the opposite behavior from the compiler—if you try to return a value from a void function then the compiler will complain.

If you don’t explicitly specify the return type of the function, you still get TypeScript’s best type inference based on the return value, which gives you some type protection:

function addThings(a: number, b: number) { return a + b }

let result: string = addThings(2, 3) // this will be a compiler error

In this case, TypeScript infers that the return value of the addThings function is a number because the returned value is the sum of two numbers. Later, assigning that value to the result variable, declared to be a string, will cause a compiler error, even though the return value of addThings is not explicitly specified.

My recommendation is to develop the habit of explicitly specifying the return type in a function that returns values. Not only is specifying the return type better for communication, if you are relying on type inference, the type system won’t catch if you accidentally return the wrong value or forget to return a value at all.

TypeScript function arguments behave differently than regular JavaScript in that the number of arguments to the function—the technical term is the arity of the function—is explicitly checked. So the following is valid JavaScript, but invalid TypeScript:

function addThings(a, b) {
  return a + b
}

const result = addThings(2, 3, 4)

The function is declared with two arguments, but we call it with three arguments. TypeScript flags this as a compiler error, even though we haven’t specified any type information on any of this code. In plain JavaScript, the third argument would be silently ignored.

There are several legitimate cases where you might want to send different sets of arguments to the same function, and TypeScript offers a few different features to cover those cases.

First off, you can specify an argument as optional with the ?: syntax, as in:

function addThings(a: number, b: number, c?: number) {
  return a + b
}

const result = addThings(2, 3)

In this case, we’ve specified the c argument as optional, meaning that we can call addThings with only two arguments.

However, the optional argument is set to the value undefined if not used, and that might not be the most helpful. So TypeScript allows for a default value for an argument using basically the same syntax as Ruby:

function addThings(a: number, b: number, c: number = 0) {
  return a + b + c
}

const result = addThings(2, 3)

In this example, we can now safely add all three arguments because if the third argument is not specified, the default value takes over and c is set to 0. If you leave the type annotation off of the default argument, function addThings(a: number, b: number, c = 0, then TypeScript uses inference to determine the type.

Optional arguments need to come at the end of the list of arguments, but arguments with default values can come anywhere in the list. Somewhat awkwardly, you can then trigger the default behavior by explicitly passing undefined to that argument.

Sometimes you legitimately want to allow an arbitrary number of arguments to a function, which you can do in TypeScript with the spread operator, ...:

function addThings(a: number, ...others: number[]) {
  return a + others.reduce((sum, value) => sum + value)
}

const result = addThings(2, 3, 4, 5, 6)

This time, any arguments passed to the method after the first one are accumulated into the others argument, which has an array type. We can then use that array like any other variable.

Functions as values

It’s pretty common in JavaScript to pass functions as arguments to other functions or to return functions as the result of a function. And, in TypeScript, that means that we need to be able to specify the types of functions when used as arguments or return values.

This gets a little meta, but the type of a function is based on the type of the arguments and the return value. We’ve already seen the syntax for this in the constructor of our GenericToggle class, back in the Going Generic chapter:

constructor(
    private buttonSelector: string,
    private targetSelector: string,
    private onToggle?: (
      button: HTMLElement,
      target: HTMLElement,
      state: boolean
    ) => void
  ) {
  // .. stuff
  }

The third argument there, onToggle, has the type onToggle: (button: HTMLElement, target: HTMLElement, state: boolean) => void, meaning that it can be any function that takes three arguments, two HTMLElements and one boolean and then returns void.

There are a few quirks to this syntax to be aware of.

First, the type information of the function is carried by the type annotations and not by the names of the arguments. When we pass a function to the GenericToggle constructor, it matters that the three arguments have the right type, not that the arguments have to be named button, target, and state.

Second, as far as the type system is concerned, there’s no difference between an optional argument denoted with ?: and a default argument denoted with say, result = 0. From the type system’s perspective, both of those forms mean that the method being described has an optional number of parameters.

Third, you can use type inference to put the function type signature on either side of an assignment.

We’ve already seen this:

let priceOfTicket = function(row: number, accessType: string) : number { }

This form is equivalent:

let priceOfTicket: (row: number, accessType: string) : number =
  function(row, accessType) { /* function body */ }

In the second form, the type signature is on the side with the variable, not the side with the value. TypeScript uses that information to infer the types in the actual function.

Get hands-on with 1200+ tech skills courses.