Functions with Types

Annotating a function's parameters and return value so TypeScript can check every call against them.

What is it?

A function is really a contract: "give me these inputs, and I'll give you this output." In plain JavaScript, that contract only exists in comments or in your head — nothing stops a caller from breaking it by passing the wrong kind of argument, or too few of them.

TypeScript lets you write that contract directly into the function signature. Each parameter gets a type annotation, and the function itself gets a return type annotation, so both what goes in and what comes out are checked. You can also mark a parameter optional with a ?, meaning callers may leave it out — and if they do, its value inside the function is undefined.

Explain like I'm 10

A typed function signature is like a vending machine's slot: it's shaped to accept coins of a specific size, and clearly labeled with what it dispenses. Try to push in the wrong shaped object and it simply won't go in — you find out immediately, not after the machine jams.

Examples

Typed parameters and return value

function calculateTotal(price: number, taxRate: number): number {
  return price + price * taxRate;
}

calculateTotal(100, 0.07); // 107
calculateTotal("100", 0.07);
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.

TypeScript checks both the types passed in and the type returned. If the function's body tried to return "total" instead of a number, that would also be flagged as an error.

Optional parameters and default values

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

greet("Mina");              // "Hello, Mina!"
greet("Mina", "Welcome");   // "Welcome, Mina!"

// A default value makes a parameter optional automatically:
function greetWithDefault(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

The ? after greeting means callers may omit it entirely, in which case it's undefined inside the function. Giving a parameter a default value achieves a similar effect, while also supplying a fallback automatically.

How it works

When the compiler checks a function call, it lines up each argument you pass against the corresponding parameter's declared type, in order, and flags any mismatch — including passing too many required arguments or too few. It does the same for the return value: every return statement inside the function body is checked against the declared return type. Optional parameters (marked with ?) must come after all required ones, since position is how arguments are matched to parameters.

Why does it exist?

Function signatures are one of the most common places bugs sneak in — passing arguments in the wrong order, forgetting one, or assuming a function returns something it doesn't. By making the contract explicit and checkable, TypeScript catches those mistakes at the call site, immediately, instead of only when the function actually runs with bad data.

When to use it

Annotate parameter types on essentially every function you write, especially ones used in more than one place or exposed to other parts of a codebase. Return type annotations are optional (TypeScript can usually infer them), but adding them explicitly is useful for documenting intent and catching a case where the function body accidentally returns the wrong type.

When not to use it

For a tiny, throwaway inline callback (like the anonymous function passed to array.map(x => x * 2)), TypeScript is usually smart enough to infer the parameter type from context, so an explicit annotation would just be unnecessary noise.

Common mistakes

  • Placing an optional parameter before a required one, which TypeScript doesn't allow — optional parameters must come last.

  • Confusing an optional parameter (name?: string) with a parameter that has a default value (name: string = "Guest") — the former can be undefined inside the function, the latter never is.

  • Assuming a return type annotation changes what the function actually returns at runtime — it only adds a compile-time check; the function's logic still determines the real value.

Practice exercises

  1. Easy:

    Write a function square(n: number): number that returns n squared, and call it correctly and incorrectly to see the error.

  2. Medium:

    Write a function formatName(first: string, last: string, middle?: string): string that combines the names, handling the case where middle is omitted.

  3. Hard:

    Write a function createUser(name: string, role: string = "member") that returns an object matching a User interface you define, and explain why the role parameter doesn't need a ? even though it's optional to callers.

Interview questions

What happens if you call a TypeScript function with the wrong number of arguments?

The compiler reports an error at the call site — too few required arguments, or too many, are both caught before the code runs.

What's the difference between an optional parameter and one with a default value?

An optional parameter (x?: T) may be omitted, and is undefined inside the function if it is. A parameter with a default value (x: T = val) may also be omitted, but then automatically takes on the default value instead of being undefined.

Is a function's return type annotation required?

No — TypeScript can usually infer the return type from the function body. Writing it explicitly is optional but often used for clarity and as an extra safety check.

Why must optional parameters come after all required ones in a function signature?

Arguments are matched to parameters by position, not by name. If an optional parameter came first, TypeScript (and JavaScript) would have no way to tell whether a single argument passed in was meant to fill the optional slot or the required one after it.

Given `function greet(name: string, greeting?: string) { return greeting ?? "Hi"; }`, what is the declared type of `greeting` inside the function body?

string | undefined — marking a parameter optional with ? doesn't just make it omittable, it also adds undefined as a possible type for it inside the function, which is exactly why greeting ?? "Hi" is needed to handle the omitted case.

Is `function f(a?: number, b: number) {}` valid TypeScript?

No — it's a compile error. Optional parameters must come after all required parameters, and here a is optional while b, which comes after it, is required, which TypeScript rejects outright.

Can a parameter have both a `?` and a default value, like `function f(x?: number = 5) {}`?

No — this is a compile error. A default value already implies the parameter is optional, since callers can omit it and get the default, so adding ? on top is redundant and TypeScript disallows combining the two.

How do you type a function that accepts any number of arguments, like `sum(1, 2, 3, 4)`?

With a rest parameter: function sum(...nums: number[]): number. Every argument passed after the fixed parameters, if any, is collected into nums as an array, and each one is checked against the declared element type.

How would you type a variable meant to hold a function, rather than a function declaration itself — for example, a variable that should only ever be assigned an operation on two numbers?

With a function type annotation: let op: (a: number, b: number) => number;. Any function later assigned to op is checked against that exact parameter and return signature, just as if it were a parameter typed the same way.

Why does TypeScript usually let you skip annotating the parameter type inside a callback like `array.map(x => x * 2)`?

This is contextual typing: TypeScript already knows the array's element type, and infers that map's callback parameter x must be that same element type from the surrounding context, so an explicit annotation would just repeat what the compiler already knows.

If `calculateTotal(price: number, taxRate: number): number` is called as `calculateTotal(100)`, what happens?

A compile error — TypeScript requires every parameter without a ? or default value to be supplied. Since taxRate isn't optional here, calling with only one argument doesn't satisfy the function's declared signature.

Given `function createUser(name: string, role: string = "member") {}`, does a caller have to know or write anything different from calling a required-parameter function?

No — from the caller's side, omitting role is exactly like an optional parameter, so createUser("Kai") is valid. Inside the function, though, role is typed as plain string, never undefined, because the default guarantees it always has a real value by the time the body runs.

What does annotating a function's return type as `: void` communicate, versus `: undefined`?

void means the function's return value isn't meant to be used at all — it signals that nothing useful comes back, and is the conventional annotation for functions run purely for their side effects. undefined as a return type is a narrower, more literal claim that the function returns exactly the value undefined, which is rarely what you actually want to express.

If a function's body has a `return "done";` statement but its signature declares `: number`, what happens?

TypeScript reports a compile error on that return statement, since the value returned doesn't match the declared return type — the mismatch is caught by checking the function body against its own signature, the same way call sites are checked against parameter types.

Does an explicit return type annotation change what value a function actually returns at runtime?

No — it only adds a compile-time check that the function body's return statements match the declared type. The function's logic is what actually determines the real returned value; the annotation can't alter or coerce it.

You pass an object literal directly into a function parameter typed with an interface, and it has one extra property the interface doesn't list. Does this behave differently than passing a variable holding the same object?

Yes — passing a fresh object literal directly at the call site triggers TypeScript's excess property check and errors on the extra property, while passing a variable that was assigned the same object earlier does not, because the stricter literal check only applies right where an object literal is written.

Why is a typed function signature sometimes compared to a vending machine's coin slot?

The slot, meaning the parameter types, is shaped to accept only specific inputs, and what comes out, the return type, is fixed and known in advance — feeding in the wrong shape of input is rejected immediately, rather than being accepted and causing a jam (a runtime error) later.

Between annotating every function parameter and every function's return type, which matters more for catching call-site mistakes, and why?

Annotating parameters matters more day-to-day, since most call-site mistakes, such as a wrong type or wrong argument count, are caught right there when arguments are checked against parameter types. Return type annotations are more optional because TypeScript can usually infer them correctly from the function body anyway, though writing them explicitly still documents intent and catches an accidental wrong-type return.