Skip to content

TypeScript lesson 5 of 9

Functions and Types

How TypeScript requires a type on every function parameter, how return types are checked against what a function actually returns, and how optional and default parameters change a parameter's type.

Published · Every example on this page was run before it was published.

A JavaScript function will accept anything you hand it - the wrong number of arguments, the wrong kind of value, values that make no sense together - and only fail once something inside the function body actually tries to use them. A TypeScript function can say, right in its own definition, exactly what it accepts and exactly what it hands back, and every call site is checked against that promise before the program runs at all.

Typing Parameters

A parameter's type is written the same way a variable's is: a colon, then the type, right after the parameter's name.

TypeScript
function area(width: number, height: number): number {
  return width * height;
}

console.log(area(4, 5));
Output
20

Both width and height must be numbers at every call site, and the compiler checks that at the call, not when the multiplication inside the function eventually runs.

Parameters Must Be Typed

Unlike a plain variable, a function parameter has no initializer for the compiler to infer a type from - its actual value only arrives later, whenever someone calls the function. Leaving a parameter's type off entirely is a compile error, not a silent fallback to "anything goes":

TypeScript
function double(n) {
  return n * 2;
}
Output
main.ts(1,17): error TS7006: Parameter 'n' implicitly has an 'any' type.

n genuinely could be given any type without an annotation to rule anything out, and TypeScript specifically refuses to let that pass unnoticed - a function whose inputs are not typed cannot promise anything about how it will behave, which defeats most of the reason to write TypeScript at all.

Typing the Return Value

A return type is optional - TypeScript will infer one from what the function's body actually returns - but writing it down catches a mismatch between what a function is supposed to give back and what it really does, right at the point the mistake was made rather than wherever the caller happens to use the result:

TypeScript
function double(n: number): number {
  return "not a number";
}
Output
main.ts(2,3): error TS2322: Type 'string' is not assignable to type 'number'.

Without the : number on the function itself, this same body would have compiled fine, and the return type would simply have been inferred as string - which might not be caught until much later, wherever the result of double(...) gets used as though it were numeric.

Optional Parameters

A ? after a parameter's name makes it optional to supply at the call site. Its type inside the function body becomes the declared type or undefined, because a caller genuinely might not pass it:

TypeScript
function greet(name: string, title?: string): string {
  if (title) {
    return `Hello, ${title} ${name}`;
  }
  return `Hello, ${name}`;
}

console.log(greet("Rao"));
console.log(greet("Rao", "Dr."));
Output
Hello, Rao
Hello, Dr. Rao

Using title as though it were definitely a string, without first checking for undefined, is exactly the kind of thing the compiler catches:

TypeScript
function shout(title?: string): string {
  return title.toUpperCase();
}
Output
main.ts(2,10): error TS18048: 'title' is possibly 'undefined'.

The if (title) check in greet above is not just a style choice - it is what makes title.toUpperCase() safe to write on the line after it. A later lesson covers this kind of check, called narrowing, in more depth.

Default Parameters

A default value can stand in for ? when there is a sensible value to fall back on instead of allowing undefined at all. The parameter's type is inferred from the default, exactly the way a plain variable's type is inferred from its initial value:

TypeScript
function greet(name: string, greeting = "Hello"): string {
  return `${greeting}, ${name}!`;
}

console.log(greet("Mei"));
console.log(greet("Mei", "Hey"));
Output
Hello, Mei!
Hey, Mei!

greeting's type is string, inferred from "Hello", so a caller may either omit it entirely or supply a matching string - anything else is rejected the same way any other type mismatch would be.

Common Mistakes

Leaving a parameter untyped and assuming the compiler will infer it, the way it does for variables. There is no initializer on a parameter to infer from, so a bare parameter is never inferred - it is always an error under the rules this site's sandbox uses, as shown above. Every parameter needs either an explicit type or a default value to infer one from.

Declaring a return type and then having a code path that never reaches a return at all. TypeScript checks that every possible path through a function actually returns a value matching the declared return type:

TypeScript
function classify(n: number): string {
  if (n > 0) {
    return "positive";
  }
}
Output
main.ts(1,31): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.

If n is 0 or negative, execution falls off the end of the function without hitting a return at all, which would hand back undefined at runtime - a value classify's declared return type, string, does not allow. Adding an else branch (or a final return after the if) fixes it by making sure every path ends in a string.

Confusing an optional parameter with a default parameter. Both let a caller skip an argument, but they leave the function with different types to work with inside the body: an optional parameter (title?: string) is string | undefined, and code using it has to check for undefined before treating it as text, the way greet's first version did above. A default parameter (greeting = "Hello") is just string - there is no undefined case to check for, because the default value fills that gap automatically before the body ever runs.

Next Steps

The nth-fibonacci-memoized practice problem is built almost entirely out of function calls, return values, and default parameters (typically a cache object with a default value), which makes it a solid place to put this lesson to work.

In the TypeScript playground, write a small function with one optional parameter and one default parameter, and call it three different ways - with nothing extra, with the optional argument supplied, and with the default argument overridden. Then delete the return type annotation and compare what an editor infers against what you intended.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.