Function Types & this

Reviewed & published by Brayan K

TypeScript lets you describe functions as values — a function type names a callable's parameters and return type, a typed this pins down the calling context, and Parameters<F> / ReturnType<F> extract those pieces back out.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A function type is a job description: "must take two numbers, must return a number." Any worker (function) who fits can be hired for the role. this is "whose desk you're sitting at" when you do the job — it changes depending on who called you in. And Parameters / ReturnType are reading the job description back off the contract: what inputs it demands and what it promises to deliver.

1. Function Type Expressions & Signatures

A function type expression like (a: number, b: number) => number names a callable shape, so any matching function — arrow or declaration — is assignable to it. For functions that also carry properties, an interface call signature works; for things you build with new, a construct signature (new (...) => T) describes the constructor.

// A function TYPE EXPRESSION names a callable shape:
type BinaryOp = (a: number, b: number) => number;

const add: BinaryOp = (a, b) => a + b;       // a, b inferred as number
const multiply: BinaryOp = (a, b) => a * b;

function compute(op: BinaryOp, a: number, b: number): number {
  return op(a, b);
}

// A CALL signature (object form) can also carry properties:
interface Counter {
  (start: number): number; // callable
  reset(): void;           // plus a method
}

// A CONSTRUCT signature describes something you call with 'new':
type CtorOf<T> = new (...args: any[]) => T;
// A FUNCTION TYPE describes a function's shape: its parameters and return type.
// In .ts:  type BinaryOp = (a: number, b: number) => number;
// Any function matching that shape is assignable. At runtime they're just functions.

const add = (a, b) => a + b;
const multiply = (a, b) => a * b;

// A higher-order function that ACCEPTS a function of that shape:
function compute(op, a, b) {
  return op(a, b);
}

console.log(compute(add, 2, 3));      // 5
console.log(compute(multiply, 2, 3)); // 6

// Store typed functions in a lookup, like a strategy table:
const ops = { add, multiply };
console.log(ops.add(10, 4));      // 14
console.log(ops.multiply(10, 4)); // 40

2. Typing this

In JavaScript, this depends on how a function is called, which trips up both people and the type-checker. TypeScript lets you declare a fake first parameter named this to pin down what it must be — function increment(this: Counter). It's erased at runtime and never passed by callers; it just makes this.count type-safe and silences noImplicitThis.

// The 'this' parameter is a FAKE first parameter — it vanishes at runtime but
// tells TS what 'this' must be. It's never passed by callers.
interface Counter { count: number; increment(this: Counter): number }

const counter: Counter = {
  count: 0,
  increment() {
    this.count += 1; // ✅ 'this' is typed as Counter
    return this.count;
  },
};

// With "strict" + "noImplicitThis", a stray 'this' is an error until you type it:
function bad() {
  // return this.count; // ❌ 'this' implicitly has type 'any'
}
// 'this' inside a method depends on HOW the function is called. In .ts you can
// type it with a fake first parameter:  function describe(this: Counter): string
// At runtime, 'this' is the object before the dot — watch it change.

const counter = {
  count: 0,
  increment() {
    this.count += 1;   // 'this' is 'counter' when called as counter.increment()
    return this.count;
  },
};

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

// Arrow functions DON'T rebind 'this' — they capture it from where defined:
const obj = {
  label: "obj",
  makeArrow() {
    return () => this.label; // arrow keeps 'this' = obj
  },
};
const getLabel = obj.makeArrow();
console.log(getLabel()); // obj

3. Parameters<F>, ReturnType<F> & Overloads

Two built-in utilities read a function's type apart: Parameters<F> gives the argument list as a tuple, and ReturnType<F> gives what it returns. Pair them with typeof fn so your types follow the function automatically. When a return type must vary by input shape, reach for overloads instead of a single union parameter.

function createUser(name: string, age: number) {
  return { id: Math.floor(age), name, age };
}

// Parameters<F> = the parameter list as a TUPLE type:
type CreateArgs = Parameters<typeof createUser>; // [string, number]

// ReturnType<F> = whatever F returns:
type User = ReturnType<typeof createUser>;       // { id: number; name: string; age: number }

// Reuse them so types never drift from the function:
function logUser(...args: CreateArgs): User {
  const u = createUser(...args);
  return u;
}

// OVERLOADS vs UNIONS: overloads give DIFFERENT return types per input shape,
// which a single union parameter can't express as precisely:
function len(x: string): number;
function len(x: unknown[]): number;
function len(x: string | unknown[]): number { return x.length; }
// Parameters<F> and ReturnType<F> EXTRACT a function's parameter tuple and return
// type. They're compile-time, but the runtime intuition is clear: reuse a
// function's arguments (a tuple) and reason about what it returns.

function createUser(name, age) {
  return { id: Math.floor(age), name, age };
}

// Runtime stand-in for Parameters<typeof createUser>: collect args as a tuple,
// forward them with the spread operator.
const args = ["Ada", 36];         // like Parameters<typeof createUser> = [string, number]
const user = createUser(...args); // forward the tuple
console.log(user.name, user.age); // Ada 36

// Runtime stand-in for ReturnType<typeof createUser>: just the returned value.
const result = createUser("Grace", 41);
console.log(Object.keys(result)); // [ 'id', 'name', 'age' ]
console.log(result.id);           // 41

🎯 Your Turn

Write a higher-order function that takes a function-typed callback and applies it twice. Fill in the two blanks marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// In .ts:  function applyTwice<T>(fn: (x: T) => T, x: T): T
// applyTwice takes a function and a value, and applies the function TWICE.

function applyTwice(fn, x) {
  return fn(fn(___));            // 👉 the starting value x goes in the inner call
}

console.log(applyTwice((n) => n + 1, 5));   // 7   (5 -> 6 -> 7)
console.log(applyTwice((s) => s + "!", ___)); // 👉 pass the string "hi" -> hi!!

// ✅ Expected output:
//    7
//    hi!!

Common Errors (and the fix)

Pro Tips

📚 Keep Learning

Mini-Challenge: A Strategy Table of Formatters

No blanks this time — just a brief and a starting outline. Build the dispatcher of function-typed formatters yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: a strategy table of function-typed formatters.
// In .ts each entry is typed (s: string) => string.
// 1. Build 'formatters': upper(s)->UPPER, lower(s)->lower,
//    reverse(s)->characters reversed.
// 2. Write format(name, text): look up formatters[name]; call it if present,
//    otherwise return text unchanged.
// 3. Log format("upper", "hello"), format("reverse", "abc"),
//    and format("unknown", "kept").
//
// ✅ Expected output:
//    HELLO
//    cba
//    kept

// your code here

🎉 Lesson Complete

Practice quiz

What does a function type expression describe?

  • A function's name only
  • Its runtime performance
  • A function's parameters and return type (its shape)
  • Its source file

Answer: A function's parameters and return type (its shape). Something like '(a: number, b: number) => number' names a callable shape.

Any function matching the parameters and return type of 'type BinaryOp = (a: number, b: number) => number' is:

  • Assignable to BinaryOp
  • Rejected
  • Converted to a class
  • Made readonly

Answer: Assignable to BinaryOp. Structural typing means any function with matching shape is assignable to the function type.

How do you type 'this' inside a function?

  • With a real first argument
  • With the bind keyword
  • You cannot
  • With a fake first parameter named 'this' (e.g. function f(this: Counter))

Answer: With a fake first parameter named 'this' (e.g. function f(this: Counter)). A 'this' parameter is a fake first parameter; it is erased at runtime and never passed by callers.

Which strict-mode flag does a 'this' parameter help satisfy?

  • noImplicitAny
  • noImplicitThis
  • strictNullChecks
  • exactOptionalPropertyTypes

Answer: noImplicitThis. Typing 'this' fixes 'this implicitly has type any' errors under noImplicitThis.

What does Parameters<F> extract?

  • The parameter list as a tuple type
  • The return type
  • The function name
  • The number of arguments

Answer: The parameter list as a tuple type. Parameters<F> gives the argument list as a tuple type.

What does ReturnType<F> extract?

  • The parameter tuple
  • The this type
  • Whatever the function returns
  • The arity

Answer: Whatever the function returns. ReturnType<F> gives the function's return type.

Why pair these utilities with 'typeof someFunction'?

  • For faster runtime
  • So derived types stay in sync with the actual function automatically
  • To enable decorators
  • To avoid imports

Answer: So derived types stay in sync with the actual function automatically. Parameters<typeof fn> / ReturnType<typeof fn> follow the function, so types update when it changes.

Do arrow functions create their own 'this'?

  • Yes, always
  • Only in strict mode
  • Only as methods
  • No — they capture 'this' lexically from where they are defined

Answer: No — they capture 'this' lexically from where they are defined. Arrow functions do not rebind this; they reuse the surrounding this, ideal for callbacks.

What describes a value you call with 'new'?

  • A call signature
  • A construct signature, e.g. new (...args: any[]) => T
  • A this parameter
  • An index signature

Answer: A construct signature, e.g. new (...args: any[]) => T. A construct signature (new (...) => T) describes a constructor.

When should you choose overloads over a union parameter?

  • Always
  • When there is one argument
  • When the return type must vary by input shape
  • Never

Answer: When the return type must vary by input shape. Overloads express different return types per input; a single union parameter cannot.

Continue this course

Frequently asked questions

What's a function type expression?

It's a way to name the shape of a function — its parameters and return type — like 'type BinaryOp = (a: number, b: number) => number'. Any function with matching parameters and return type satisfies it, which makes function types perfect for callbacks and higher-order functions. For callables that also carry properties, use a call signature in an interface instead.

How do I type 'this' in a function?

Add a fake first parameter named 'this' with the type you expect: 'function describe(this: Counter): string'. It's purely a compile-time annotation — callers never pass it, and it disappears at runtime. With strict mode (noImplicitThis), this is how you fix 'this implicitly has type any' errors in standalone functions and methods.

What do Parameters<F> and ReturnType<F> do?

Parameters<F> extracts a function type's parameter list as a tuple type, and ReturnType<F> extracts what it returns. Combined with 'typeof someFunction', they let you derive argument and result types directly from an existing function, so your types stay in sync automatically when the function changes.

When should I use overloads instead of a union parameter?

Use overloads when the return type depends on which input shape you got — e.g. a function that returns a string for a string input but a number for a number input. A single union parameter forces one return type for all cases. Overloads let you express several precise (input -> output) relationships, with one implementation signature underneath.

Related lessons