Getting started General
Components
Forms
Trends
Utilities
Plugins Sass Migrate from v1
  Join us
  JavaScript Articles

TypeScript Function Type: Complete Guide with Examples

JavaScript

Learn the TypeScript function type syntax with clear examples: declare function, method types, callbacks, generics, overloads and how to type any function safely.

TypeScript function type syntax: parameters, arrow and return type
TypeScript Function Type: Complete Guide with Examples

A TypeScript function type describes the parameters a function accepts and the value it returns. You write it as a parameter list, a fat arrow, then the return type:

type Sum = (a: number, b: number) => number

const sum: Sum = (a, b) => a + b

That one line covers most day-to-day needs, but functions are also where TypeScript hides most of its surprises: methods are checked differently from function properties, void does not mean what you think, and the built-in Function type is a trap. This guide goes through every way to type a function in TypeScript, in the order you will need them. Every snippet and every error message below was checked against the TypeScript compiler in strict mode.

What Is a Function Type in TypeScript?

In JavaScript, functions are values. You can store them in variables, pass them as arguments and return them from other functions. Because they are values, they need a type, and that type is the function's signature: the type of each parameter plus the return type.

Take this plain JavaScript function:

function sum(a, b) {
  return a + b
}

It takes two numbers and returns a number. The matching function type expression is:

(a: number, b: number) => number

It looks like an arrow function, but it is a type: the right side of => is the return type, not a function body. Give it a name with a type alias and you can reuse it anywhere a type is expected:

type Sum = (a: number, b: number) => number

// As a variable type
const add: Sum = (a, b) => a + b

// As a parameter type (a callback)
function calculate(operation: Sum): number {
  return operation(4, 3)
}

// As a return type
function createSum(): Sum {
  return (a, b) => a + b
}

calculate(add) // 7
createSum()(1, 2) // 3

Notice that a and b have no annotations in the implementations. Once a function is assigned to something with a function type, TypeScript infers the parameter types from that type. This is called contextual typing, and it is the reason typed callbacks are so pleasant to write.

Parameter Names Are Required in the Type

The most common mistake when writing a function type for the first time is leaving out the parameter name:

type Log = (string) => void
// error TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?

TypeScript reads (string) => void as "a parameter named string with no type". Without strict mode this compiles silently and the parameter becomes any, which is worse. Always write (message: string) => void. The name itself is documentation only: the implementation is free to call the parameter something else.

How to Declare a Function in TypeScript

There are three ways to write a function, and they all produce the same function type. What changes is where the annotations go.

// 1. Function declaration: annotate parameters and return type inline
function sum(a: number, b: number): number {
  return a + b
}

// 2. Function expression
const sum2 = function (a: number, b: number): number {
  return a + b
}

// 3. Arrow function
const sum3 = (a: number, b: number): number => a + b

// All three have the type (a: number, b: number) => number

The return type annotation is optional because TypeScript infers it from the return statements. Writing it anyway is a good habit for exported functions: the error shows up inside the function that returns the wrong thing instead of in every file that calls it.

A function declaration cannot be annotated with a type alias directly, since there is no variable to attach the type to. If you want a declaration checked against an existing function type, use satisfies on an expression or simply assign it:

type Sum = (a: number, b: number) => number

const sum = ((a, b) => a + b) satisfies Sum

function plainSum(a: number, b: number) {
  return a + b
}
const checked: Sum = plainSum // OK, the signatures match

Function declarations are also hoisted, which arrow functions stored in const are not. If that distinction is fuzzy, our JavaScript hoisting guide covers it in detail.

The declare function Keyword

declare function is a different thing. It tells the compiler that a function exists at runtime without providing a body. Nothing is emitted in the JavaScript output. You use it for functions that come from somewhere TypeScript cannot see: a script tag, a global injected by your bundler, or an untyped library.

// globals.d.ts
declare function gtag(command: 'config', id: string): void
declare function gtag(
  command: 'event',
  name: string,
  params?: Record<string, unknown>
): void

// anywhere in your app
gtag('event', 'signup') // OK, fully typed

Two rules to remember: a declare function never has a body, and several declarations with the same name form overloads. Inside a .d.ts file, top-level functions and variables must start with declare or export.

Typing Function Parameters

Optional and Default Parameters

Add ? after the name to make a parameter optional. Its type becomes number | undefined inside the function, so you have to handle the missing case.

type Sum = (a: number, b?: number) => number

const sum: Sum = (a, b = 0) => a + b

sum(2) // 2
sum(2, 3) // 5

Default values belong to the implementation only. A function type cannot contain b = 0, so mark the parameter optional in the type and set the default in the function, as above. Optional parameters must come after all required ones.

Rest Parameters

A rest parameter is typed with an array type. Use a tuple type when the number and position of the arguments are fixed.

type SumAll = (...numbers: number[]) => number

const sumAll: SumAll = (...numbers) => numbers.reduce((total, n) => total + n, 0)
sumAll(1, 2, 3) // 6

// Mixed: one required parameter, then any number of strings
type Log = (level: 'info' | 'error', ...messages: string[]) => void

// Tuple: exactly 2 or 3 arguments, with labels
type ToPoint = (...coords: [x: number, y: number, z?: number]) => string

Destructured Parameters

The annotation goes after the whole destructuring pattern, never inside it. Writing { url: string } inside the pattern would rename url to a variable called string.

type RequestOptions = { url: string; retries?: number }

function request({ url, retries = 3 }: RequestOptions): Promise<Response> {
  return fetch(url)
}

Typing the Return Value

Return type Use it when Example
void The function returns nothing useful (message: string) => void
Promise<T> The function is async or returns a promise (id: string) => Promise<User>
never The function always throws or never finishes (message: string) => never
value is T The function is a type guard (value: unknown) => value is string
// Async functions always return a Promise
type SumAsync = (a: number, b: number) => Promise<number>

const sumAsync: SumAsync = async (a, b) => a + b // OK
const wrong: SumAsync = (a, b) => a + b
// error TS2322: Type 'number' is not assignable to type 'Promise<number>'.

// never: the code after a call to fail() is unreachable
function fail(message: string): never {
  throw new Error(message)
}

// Type guard: narrows the argument in the caller
const isString = (value: unknown): value is string => typeof value === 'string'

TypeScript Method Type

A method is a function that lives on an object. Its type is written inside an object type or an interface, and there are two syntaxes for it:

interface Calculator {
  // Method syntax: colon before the return type
  sum(a: number, b: number): number

  // Property syntax: a property holding a function type
  product: (a: number, b: number) => number

  // Optional method
  reset?(): void
}

const calculator: Calculator = {
  sum(a, b) {
    return a + b
  },
  product: (a, b) => a * b
}

Both describe something you can call as calculator.sum(1, 2), and the same syntax works in a type alias. In a class, you annotate methods exactly like function declarations:

class Counter {
  count = 0

  increment(): void {
    this.count++
  }

  // Arrow function property: `this` is always the instance
  decrement = (): void => {
    this.count--
  }
}

Method Syntax vs Property Syntax: Not the Same Check

Most tutorials present the two syntaxes as interchangeable. They are not. With strictFunctionTypes (included in strict), parameters of function type properties are checked strictly, while parameters of methods are checked loosely (bivariantly) for backward compatibility. The loose check lets unsafe code through:

interface Animal { name: string }
interface Dog extends Animal { bark(): void }

interface WithMethod {
  handle(animal: Animal): void
}

interface WithProperty {
  handle: (animal: Animal) => void
}

const dogHandler = (dog: Dog) => dog.bark()

const a: WithMethod = { handle: dogHandler } // OK?! Will crash on a cat
const b: WithProperty = { handle: dogHandler }
// error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
//   Types of parameters 'dog' and 'animal' are incompatible.
//     Property 'bark' is missing in type 'Animal' but required in type 'Dog'.

WithMethod accepts a handler that only works for dogs even though it promises to handle any animal. WithProperty catches the bug. For callbacks and event handlers stored on objects, prefer the property syntax. Keep the method syntax for real methods that rely on this.

Call Signatures: Function Interfaces

A function type expression has a longer form called a call signature. It is an object type with a parameter list and no name:

interface Sum {
  (a: number, b: number): number
}

// Strictly equivalent to
type Sum2 = (a: number, b: number) => number

For a plain function, the arrow form is shorter and reads better. The call signature becomes necessary in two cases: when the function also carries properties, and when it has several overloads (covered below).

interface SumWithDescription {
  (a: number, b: number): number
  description: string
}

// The properties must exist at assignment time
const sum: SumWithDescription = Object.assign(
  (a: number, b: number) => a + b,
  { description: 'Adds two numbers' }
)

sum(1, 2) // 3
sum.description // 'Adds two numbers'

A frequent error here is to assign the bare function first and add the property on the next line. That fails with Property 'description' is missing in type '(a: number, b: number) => number', because the value is incomplete at the moment it is assigned. Object.assign builds the complete value in one expression.

Construct Signatures

Put new in front of a signature to describe something that must be called with new, typically a class passed as an argument:

type Constructor<T> = new (...args: any[]) => T

function create<T>(Ctor: Constructor<T>): T {
  return new Ctor()
}

class Logger {}
const logger = create(Logger) // Logger

How to Type a Callback That Accepts Any Function

Sooner or later you write a helper that takes "some function" without caring about its exact signature: a debounce, a memoizer, an event bus. The goal is to accept any function but reject everything else, without falling back to any. You have three candidates.

Type Accepts Calling it returns Verdict
Function Any function, and classes any, with unchecked arguments Avoid
(...args: any[]) => any Any function, classes rejected any, with unchecked arguments Fine as a generic constraint
(...args: never[]) => unknown Any function, classes rejected unknown, and you cannot call it carelessly Safest when you only store or forward it
type AnyFunction = (...args: any[]) => any

function runLoose(callback: Function) {
  return callback(1, 2, 3) // no checks at all, returns any
}

function run(callback: AnyFunction) {
  return callback()
}

class Logger {}

runLoose(Logger) // Compiles, then throws: Class constructor Logger cannot be invoked without 'new'
run(Logger)
// error TS2345: Argument of type 'typeof Logger' is not assignable to parameter of type 'AnyFunction'.

Function is the type of Function.prototype: it matches anything callable, including classes that throw when called without new. The typescript-eslint rule no-unsafe-function-type flags it for that reason.

In practice you rarely want "any function" as a final type. You want to keep the signature the caller gave you. That is a job for a generic constrained to AnyFunction:

function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>

  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
}

const log = debounce((message: string) => console.log(message), 200)

log('saved') // OK
log(42)
// error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

The any never leaks: it only appears in the constraint, and the returned function has the exact parameters of the original.

Generic Function Types

Generics link the types of parameters to each other or to the return type. With function types, the position of the type parameter matters, and it decides who picks the type.

// Generic function: the caller picks T on every call
type Identity = <T>(value: T) => T

const identity: Identity = (value) => value
identity('a') // string
identity(1) // number

// Generic type alias: T and U are fixed when the type is used
type Mapper<T, U> = (value: T) => U

const toLength: Mapper<string, number> = (value) => value.length
toLength('hello') // 5

Use the first form for functions that work with any type (identity, map, first). Use the second to describe a family of related function types, like Mapper<User, string> and Mapper<Order, number>.

Function Overloads

When a function can be called in several distinct ways, list the signatures first and then write one implementation that handles all of them. The implementation signature is not callable from outside.

function makeDate(timestamp: number): Date
function makeDate(month: number, day: number, year: number): Date
function makeDate(monthOrTimestamp: number, day?: number, year?: number): Date {
  return day !== undefined && year !== undefined
    ? new Date(year, monthOrTimestamp, day)
    : new Date(monthOrTimestamp)
}

makeDate(1700000000000) // OK
makeDate(5, 12, 2026) // OK
makeDate(5, 12)
// error TS2575: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

To express overloads as a function type, write several call signatures in one object type:

type Convert = {
  (input: string): number
  (input: number): string
}

Do not reach for overloads too early. If the signatures only differ by the type of one argument and return the same thing, a union such as (input: string | number) => string is simpler and works with more callers.

Typing this in a Function

TypeScript lets you declare the type of this as a fake first parameter. It is erased from the JavaScript output and does not count as an argument.

interface User {
  name: string
  admin: boolean
}

type UserFilter = (this: User) => boolean

const isAdmin: UserFilter = function () {
  return this.admin
}

isAdmin.call({ name: 'Ada', admin: true }) // OK
isAdmin()
// error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'.

This only works with function expressions. Arrow functions take this from the enclosing scope, so they cannot have a this parameter. For the runtime side of the story, see our guide on the JavaScript bind method and this binding.

Assignability Rules That Surprise People

Fewer Parameters Are Allowed, More Are Not

type Sum = (a: number, b: number) => number

const first: Sum = (a) => a // OK
const zero: Sum = () => 0 // OK
const three: Sum = (a: number, b: number, c: number) => a + b + c
// error TS2322: Type '(a: number, b: number, c: number) => number' is not assignable to type 'Sum'.
//   Target signature provides too few arguments. Expected 3 or more, but got 2.

Ignoring an argument is always safe in JavaScript, and it is what you do every time you write items.map((item) => ...) without using the index. Requiring an argument that the caller will never pass is not safe, so it is rejected.

A void Callback Can Return a Value

type Callback = () => void

const callback: Callback = () => 42 // OK
const result = callback() // result is void, not number

function strict(): void {
  return 42
}
// error TS2322: Type 'number' is not assignable to type 'void'.

In a function type, void means "the return value will be ignored", not "must return nothing". That is what makes items.forEach((item) => list.push(item)) legal even though push returns a number. On a function declaration, void is strict and returning a value is an error.

Do Not Make Callback Parameters Optional

function each(items: string[], callback: (item: string, index?: number) => void) {
  items.forEach((item, index) => callback(item, index))
}

each(['a', 'b'], (item, index) => console.log(index.toFixed()))
// error TS18048: 'index' is possibly 'undefined'.

Marking index optional says "I might call you without it", which forces every caller to handle undefined. Since callbacks can already ignore extra arguments, declare the parameter as required: (item: string, index: number) => void.

Extracting Types from an Existing Function

You do not always need to write a function type by hand. TypeScript ships utility types that read it from a function you already have, which keeps the two in sync.

function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age }
}

type CreateUser = typeof createUser
// (name: string, age: number) => { id: string; name: string; age: number }

type CreateUserParams = Parameters<typeof createUser>
// [name: string, age: number]

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

async function fetchUser(id: string) {
  return { id, name: 'Ada' }
}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>
// { id: string; name: string }

TypeScript Function Type Cheat Sheet

Need Syntax
Function type (a: number, b: number) => number
Optional parameter (a: number, b?: number) => number
Rest parameters (...values: number[]) => number
No return value (message: string) => void
Async function (id: string) => Promise<User>
Method in an interface sum(a: number, b: number): number
Function with properties { (a: number): number; description: string }
Constructor new (...args: any[]) => T
Any function (...args: any[]) => any
Generic function <T>(value: T) => T
Typed this (this: User) => boolean
Type guard (value: unknown) => value is string
Ambient declaration declare function name(arg: string): void

Frequently Asked Questions

Should I use type or interface for a function type?

Use a type alias with the arrow syntax. It is shorter and it is what most codebases and the TypeScript handbook use. An interface with a call signature produces the same type and is only worth it when the function also has properties or when you need declaration merging.

What is the difference between a function type and a method type?

A function type stands alone and uses => before the return type. A method type is declared inside an object type and uses : before the return type. Beyond the syntax, method parameters are checked less strictly than function type properties under strictFunctionTypes.

Can I use the Function type in TypeScript?

You can, but you should not. Function accepts classes, does not check arguments and returns any. Write the real signature, or (...args: any[]) => any when the function can truly be anything.

What is the difference between void and undefined as a return type?

() => undefined requires the function to return undefined and lets the caller use that value. () => void accepts a function returning anything and tells the caller not to use the result. For callbacks, void is almost always the right choice.

Does declare function generate any JavaScript?

No. declare function is erased at compile time. It only describes a function that already exists at runtime. If nothing provides that function, the code compiles and then fails with a ReferenceError.

Conclusion

The TypeScript function type is a parameter list, a fat arrow and a return type: (a: number, b: number) => number. Name it with a type alias and let contextual typing fill in the parameters of your implementations.

From there, each variation answers one specific need: a method type inside an object, a call signature when the function has properties or overloads, a generic when the caller's types must be preserved, and declare function when the implementation lives outside TypeScript. Keep three rules in mind and most function type errors explain themselves: implementations may take fewer parameters but never more, void in a function type means "ignored", and function type properties are checked more strictly than methods.

For the complete reference, the functions chapter of the TypeScript handbook is the next stop. And if you model fixed sets of values next to your functions, have a look at our guide to enums in JavaScript.

Start Building with Axentix

Ready to create amazing websites? Get started with Axentix framework today.

Get Started

Related Posts