ProgrammingFrontend Developer

How to properly type a callback function in TypeScript and what pitfalls should be considered when working with context and type errors?

Pass interviews with Hintsage AI assistant

Answer.

Background: In JavaScript, callback functions are used everywhere, but their signatures are often non-obvious. In TypeScript, it is important to explicitly state the types of parameters and return values; otherwise, it is easy to create holes in type safety.

Problem: Incorrect or loose typing of callbacks leads to undefined types of arguments and results, complicates working with context (this), breaks automatic error checking by the compiler, and hampers refactoring.

Solution: It is necessary to explicitly define the type of the callback function, specify the types of passed parameters, correctly handle optional arguments and return values, and explicitly specify the context type if necessary.

Code example:

type Callback = (error: Error | null, result?: string) => void; function doAsyncWork(data: string, cb: Callback): void { setTimeout(() => { if (data === '') cb(new Error('Empty string')); else cb(null, data.toUpperCase()); }, 100); }

Key features:

  • Always specify the types of all callback parameters.
  • Describe the return type, even if it’s void.
  • Explicitly fix the type of this if necessary (for example, through a function with context).

Tricky questions.

What happens if you do not specify the return type of the callback?

TypeScript will accept any return type (e.g., undefined, void, Promise), which can lead to surprises in asynchronous chains or when returning "default" values.

type BadCallback = (data: string) => any; // any result, no control

Can you write a callback as Function or (...args: any[]) => any?

No. This removes all type safety, losing information about the number of parameters, their types, and the return type. This approach is more costly than abandoning TypeScript altogether.

How to type a function with this context?

Use the first parameter this in the function signature or cast through bind. For example:

interface ClickCallback { (this: HTMLElement, event: MouseEvent): void; } const handler: ClickCallback = function (event) { this.textContent = 'ok'; };

Type errors and anti-patterns

  • Untyped callbacks (any or Function)
  • Missing return type in function signature
  • Mismatched this type can lead to random runtime errors

Real-world examples

Negative case

In a project, the callback was declared as (...args: any[]) => any. When business logic was updated, the signature changed, and the callback stopped passing necessary arguments; bugs surfaced only in production.

Pros:

  • Easier to compile and integrate third-party code

Cons:

  • No type safety at all
  • Difficulties in updates

Positive case

Strict types were implemented: callback interfaces were described, and the type of this and return type were explicitly specified. The compiler started catching errors before deployment, refactoring was simplified, and bug fixing support improved.

Pros:

  • Type safety
  • Checking signature changes at compile time

Cons:

  • Slightly complicated typing, increased boilerplate volume