Question

Strongly Typed Function Parameters in TypeScript

Question

In TypeScript, I can declare a function parameter using the Function type, but that does not seem type-safe. For example:

class Foo {
  save(callback: Function): void {
    // Do the save
    const result: number = 42;

    // Can TypeScript ensure at compile time that the callback
    // accepts exactly one parameter of type number?
    callback(result);
  }
}

const foo = new Foo();

const callback = (result: string): void => {
  alert(result);
};

foo.save(callback);

Here, save passes a number to the callback, but the callback expects a string, and using Function does not properly protect against that mismatch.

Is there a type-safe way to define a function parameter in TypeScript so the callback signature is checked at compile time?

In short: is there a TypeScript equivalent to a strongly typed delegate in .NET?

Short Answer

By the end of this page, you will understand why using Function is too broad in TypeScript, how to replace it with a typed function signature, and how TypeScript checks callback parameter types at compile time. You will also see common callback patterns, real-world usage, and how this compares to delegates in languages like C#.

Concept

In TypeScript, the type Function means some callable value, but it does not describe the function's parameter types or return type in a useful way.

That is why this is unsafe:

save(callback: Function): void

It tells TypeScript only that callback is a function. It does not tell TypeScript:

  • how many arguments the function expects
  • what types those arguments should be
  • what the function returns

If you want type safety, you should use a function type signature.

For your example, the callback should accept one number and return void:

save(callback: (result: number) => void): void

Now TypeScript knows the exact shape of the callback. If you try to pass a function that expects a string instead of a number, TypeScript can report an error.

This matters because callbacks are everywhere in real programs:

  • event handlers
  • array methods like map and filter
  • async APIs
  • database and file operations
  • custom hooks and utility functions

If callback types are vague, mistakes slip through. If callback types are explicit, your editor can autocomplete correctly, your code becomes easier to understand, and many bugs are caught before runtime.

This is very similar to a strongly typed delegate in .NET. In TypeScript, the equivalent idea is usually expressed with:

  • an inline function type, like (value: number) => void
  • a type alias, like type SaveCallback = (value: number) => void
  • sometimes an interface with a call signature

So yes: TypeScript absolutely supports strongly typed function parameters.

Mental Model

Think of Function as writing "some tool" on a box label.

That tells you the box contains a tool, but not whether it is:

  • a hammer
  • a screwdriver
  • a wrench

A typed function signature is like labeling the box with exact instructions:

  • takes one number
  • returns nothing

Now anyone using the box knows exactly what fits.

So:

  • Function = "this is a function somehow"
  • (result: number) => void = "this function must take one number and return nothing"

The second label is what gives you safety.

Take Quiz

Syntax and Examples

Core syntax

Use a function type instead of Function:

(result: number) => void

This means:

  • the function takes one parameter named result
  • that parameter must be a number
  • the function returns void

Your example, typed safely

class Foo {
  save(callback: (result: number) => void): void {
    const result = 42;
    callback(result);
  }
}

const foo = new Foo();

const goodCallback = (result: number): void => {
  console.log(result);
};

foo.save(goodCallback);

This works because the callback matches the expected signature.

Example that fails type checking

class Foo {
  save(callback: (result: number) => void): void {
    const result = 42;
    callback(result);
  }
}

const badCallback = (result: string): void => {
  console.log(result);
};

foo.save(badCallback);

TypeScript should report a type mismatch because string is not compatible with number.

Using a type alias

This is often cleaner in real code:

type SaveCallback = (result: number) => void;

class Foo {
  save(callback: SaveCallback): void {
    const result = 42;
    callback(result);
  }
}

A type alias makes the callback reusable and easier to read.

Callback with a return value

Function types can also describe return values:

type Formatter = (value: number) => string;

const formatNumber: Formatter = (value) => `Value: ${value}`;

Here, the callback must return a string.

Step by Step Execution

Consider this code:

class Foo {
  save(callback: (result: number) => void): void {
    const result = 42;
    callback(result);
  }
}

const foo = new Foo();

const callback = (result: number): void => {
  console.log("Saved value:", result);
};

foo.save(callback);

Step by step:

  1. Foo defines a method named save.
  2. save expects one argument: a function.
  3. That function must match this shape:
    • one parameter
    • parameter type is number
    • return type is void
  4. const result = 42; creates a number.
  5. callback(result); calls the provided function with 42.
  6. Since callback accepts a number, the call is valid.
  7. The console prints:
Saved value: 42

Now compare with a bad callback:

const callback = (result: string): void => {
  console.log(result);
};

When TypeScript checks foo.save(callback), it sees:

  • save requires (result: number) => void
  • callback is (result: string) => void
  • string does not match number

So the type checker can reject it before the program runs.

Real World Use Cases

1. API request callbacks

A function may fetch data, then pass the result to a callback:

type UserCallback = (userId: number) => void;

function loadUser(callback: UserCallback): void {
  callback(101);
}

This prevents accidentally writing a callback that expects the wrong type.

2. Event processing

You may define custom event handlers:

type MessageHandler = (message: string) => void;

function onMessage(handler: MessageHandler): void {
  handler("Hello");
}

3. Data transformation

Functions like map use typed callbacks:

const numbers = [1, 2, 3];
const strings = numbers.map((n: number) => n.toString());

The callback receives a number, so your transformation is type-checked.

4. Validation logic

You can pass validation functions safely:

type Validator = (input: string) => boolean;

function validateName(name: string, validator: Validator): boolean {
  return validator(name);
}

5. Async completion handlers

Even if you later use Promises, older and custom APIs often use callbacks:

type DoneCallback = (success: boolean) => void;
Take Quiz

Real Codebase Usage

In real projects, developers rarely use raw Function unless they truly mean "any function at all." Most of the time, they write exact function signatures.

Common patterns

Guarding callback inputs

type SaveCallback = (result: number) => void;

function saveValue(value: number, callback: SaveCallback): void {
  if (value < 0) {
    return;
  }

  callback(value);
}

The guard clause handles invalid state early, and the callback stays well typed.

Reusing callback types

type ErrorHandler = (message: string) => void;
type SuccessHandler = (id: number) => void;

Teams often create named callback types for readability.

Passing callbacks into utilities

function processNumbers(
  values: number[],
  handler: (value: number) => void
): void {
  for (const value of values) {
    handler(value);
  }
}

This pattern appears in utility modules, services, and framework code.

Typed callbacks in configuration objects

type Options = {
  onSuccess: (count: number) => void;
  onError: (message: string) => void;
};

This is common in libraries and app-level services.

Error-first callback style

Some codebases still use callback conventions like this:

type LoadCallback = (error: Error | null, data: string | null) => void;

Even here, the callback is strongly typed.

Why teams prefer this

  • better autocomplete
  • clearer APIs
  • safer refactoring
  • easier code review
  • fewer runtime bugs

Common Mistakes

1. Using Function instead of a real signature

Broken:

function save(callback: Function): void {
  callback(42);
}

Why it is a problem:

  • Function is too generic
  • parameter and return types are lost

Better:

function save(callback: (result: number) => void): void {
  callback(42);
}

2. Forgetting the return type

Sometimes beginners type only the parameter list mentally, but not the full function shape.

Correct form:

(value: number) => void

Not just:

(value: number)

3. Passing a callback with the wrong parameter type

Broken:

const callback = (value: string) => {
  console.log(value);
};

save(callback);

Fix:

const callback = (value: number) => {
  console.log(value);
};

4. Confusing void with "returns nothing at runtime"

void means the caller should not rely on any returned value.

const cb: (n: number) => void = (n) => {
  console.log(n);
};

5. Making callback types harder to read than necessary

Too complex inline types can become messy.

Instead of repeating this everywhere:

(userId: number, isAdmin: boolean, name: string) => void

prefer:

type UserHandler = (userId: number, isAdmin: boolean, name: string) => void;

6. Ignoring compiler settings

Type safety is stronger when your project uses stricter TypeScript settings such as strict. In looser setups, some unsafe cases may be allowed more easily.

Comparisons

ApproachExampleType-safe?Best use
Functioncallback: FunctionNoRarely useful for app code
Inline function typecallback: (n: number) => voidYesSimple one-off callbacks
Type aliastype SaveCallback = (n: number) => voidYesReusable callback types
Interface call signatureinterface SaveCallback { (n: number): void }YesLess common, but valid

Function vs typed function signature

callback: Function
  • only says it is callable
  • does not describe parameters clearly
  • weak for API design
callback: (result: number) => void
  • describes exactly what the function must accept
  • allows compile-time checking
  • preferred in most cases

Type alias vs interface

type SaveCallback = (result: number) => void;
interface SaveCallback {
  (result: number): void;
}

Both can work. In many TypeScript codebases, type is more common for simple function signatures because it is short and readable.

TypeScript callback types vs .NET delegates

ConceptTypeScript.NET
Strongly typed callable(n: number) => voidAction<int> or custom delegate
Reusable nametype SaveCallback = ...delegate void SaveCallback(int n)
Compiler checkingYesYes

So the closest equivalent to a .NET delegate is a typed function signature, often wrapped in a type alias.

Take Quiz

Cheat Sheet

// Avoid
callback: Function

// Prefer
callback: (value: number) => void

Common patterns

type Callback = (value: number) => void;

type Formatter = (value: number) => string;

type Validator = (text: string) => boolean;

Class method example

class Foo {
  save(callback: (result: number) => void): void {
    const result = 42;
    callback(result);
  }
}

Reusable alias

type SaveCallback = (result: number) => void;

Interface version

interface SaveCallback {
  (result: number): void;
}

Rules to remember

  • Function is too broad for most callbacks.
  • Use explicit parameter and return types.
  • void means callers should ignore the return value.
  • Prefer type aliases for reusable callback signatures.
  • Strict compiler settings improve safety.

Quick mental shortcut

(param1: Type1, param2: Type2) => ReturnType

FAQ

Is Function bad in TypeScript?

Not always, but it is usually too broad for callback parameters. It loses important information about arguments and return values.

How do I type a callback function in TypeScript?

Use a function signature such as:

(callback: (value: number) => void)

Can I create a reusable callback type?

Yes. Use a type alias:

type SaveCallback = (value: number) => void;

Is there a TypeScript equivalent to a C# delegate?

Yes. A typed function signature, often stored in a type alias, is the closest equivalent.

Should I use a type alias or an interface for function types?

Either works. For simple callbacks, many developers prefer type because it is shorter and clearer.

Why did my wrong callback still compile?

It may be because you used Function, or because your TypeScript configuration is not strict enough. Exact behavior can vary with compiler settings.

Can callbacks return values too?

Yes. Example:

type Formatter = (n: number) => string;

This callback takes a number and returns a string.

Take Quiz

Mini Project

Description

Build a small TypeScript utility that simulates saving a score and then notifies the caller through a strongly typed callback. This demonstrates how to replace Function with a safe callback signature in a practical, reusable way.

Goal

Create a ScoreService class whose saveScore method accepts only callbacks that take a numeric score.

Requirements

  1. Create a class named ScoreService.
  2. Add a method saveScore that accepts a callback with one number parameter.
  3. Inside the method, simulate saving a score and pass a number to the callback.
  4. Create one valid callback and call saveScore with it.
  5. Add one invalid callback example in a comment to show what TypeScript should reject.
Take Quiz

Keep learning

Related questions

View more