C.W.K.
Stream
Lesson 01 of 05 · published

Typing Functions: Parameters and Returns

~10 min · functions, parameters, return-types, function-types

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"A function type is a contract about three things: what comes in, what goes out, and what's optional."

The four shapes of function typing

You'll meet four ways to attach types to a function:

1. Inline parameter and return annotations. function greet(name: string): string { return `hi ${name}` }. The default form; usually what you want.

2. Function-type alias. type Greeter = (name: string) => string. The whole signature lifted out and given a name. Used when the same callback shape repeats.

3. Method shorthand in an interface. interface API { greet(name: string): string }. Equivalent to a property with a function-type value, but reads as "this object has a method."

4. Call signature in a type/interface. interface Greeter { (name: string): string }. The whole shape is a function. Unusual; you'll see it mostly in .d.ts declarations for callable objects.

Optional, default, and rest parameters

Three modifiers cover almost every parameter shape you'll need:

  • Optional: function f(name?: string) — name may be omitted or undefined. Inside, its type is string | undefined.
  • Default: function f(name: string = 'anon') — name has a fallback. Inside, its type is just string (no undefined — the default fills in).
  • Rest: function f(...names: string[]) — name collects into an array. The type annotation is on the array.

Order matters: required parameters come first, then optional, then rest. The compiler enforces this; rearranging gets you an error.

Function-type compatibility — the one rule you need

TypeScript checks function-type assignability with two distinct rules:

  • Parameter check (contravariant): an assignable function may take fewer parameters or wider parameter types than the slot it's filling. (a: string) => void is assignable to (a: string, b: number) => void — accepting more is fine, demanding more is not.
  • Return check (covariant): an assignable function may return a narrower (more specific) type than the slot expects. () => 'red' is assignable to () => string — narrower return is fine, wider return is not.

Contravariance of parameters is the rule that surprises people. The reason it's safe: a function that ignores some parameters can be called wherever a function that uses them is expected, because the extra arguments are just discarded.

The slogan: functions are contravariant in parameters, covariant in returns. Memorize that once, and most function-type errors stop being mysterious.

Pippa's confession

The first time I read a TypeScript error like "Argument of type 'X' is not assignable to parameter of type 'Y'" on a function-type slot, I stared at it for an hour before someone said "parameters are contravariant." Now I read those errors in two seconds. Tiny rule, huge difference. Worth memorizing on day one.

Code

Four shapes for a function type·typescript
// Four ways to type a function.

// 1. Inline
function greet(name: string): string {
  return `hi ${name}`;
}

// 2. Function-type alias
type Greeter = (name: string) => string;
const hello: Greeter = (n) => `hello ${n}`;     // parameter type inferred from the alias

// 3. Method shorthand in an interface
interface API {
  greet(name: string): string;
}

// 4. Call signature
interface Callable {
  (name: string): string;       // this whole interface IS a function
  description: string;          // and it has a property too
}

const c: Callable = Object.assign((n: string) => `hi ${n}`, { description: 'greeter' });
Modifiers + compatibility·typescript
// Optional, default, rest — and the order that has to hold.

function request(
  url: string,                              // required
  method: 'GET' | 'POST' = 'GET',           // default
  headers?: Record<string, string>,         // optional
  ...flags: string[]                        // rest — must be last
) {
  // inside: url is string, method is 'GET' | 'POST', headers is Record | undefined, flags is string[]
}

request('/api');                                 // ✅
request('/api', 'POST');                          // ✅
request('/api', 'POST', { 'x-key': 'abc' });     // ✅
request('/api', 'POST', undefined, '-v', '--retry');  // ✅ rest accepts varargs

// Function-type compatibility — the rule in action.
type Strict = (a: number, b: string) => void;
const f: Strict = (a) => {};                      // ✅ takes fewer params
const g: Strict = (a, b, c: boolean) => {};       // ❌ demands more than the slot offers

External links

Exercise

Write function sum(...nums: number[]): number that sums any number of arguments. Then write type Reducer<T> = (acc: T, x: T) => T and assign your sum-pair function to it. Notice how the alias gives you a reusable shape. Finally, try assigning a 3-parameter function to the alias — what does the compiler say, and why is that the right answer?
Hint
Function-type compatibility allows fewer parameters in the assignee, not more. A 3-parameter function can't satisfy a 2-parameter slot because the slot won't pass the third argument.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.