TypeScript functions
How to type functions in TypeScript: parameters, return types, callbacks, generics, overloads and more.
Parameters and return types
function add(a: number, b: number): number {
return a + b
}
const multiply = (a: number, b: number): number => a * b
TypeScript always needs parameter types. It infers the return type, so you can often leave it out. Write it anyway when:
- the function is exported (the type is part of your public API, and a change to the body can’t silently change it)
- the function is recursive (TypeScript can’t infer it)
- you want an error inside the function if you return the wrong thing, rather than at every caller
Optional, default and rest parameters
function greet(name: string, greeting?: string) {
// greeting: string | undefined
return `${greeting ?? 'Hello'}, ${name}`
}
function greet2(name: string, greeting = 'Hello') {
// greeting: string (inferred from the default)
return `${greeting}, ${name}`
}
function sum(...numbers: number[]) {
return numbers.reduce((total, n) => total + n, 0)
}
Optional parameters must come after the required ones.
Destructured parameters
The type goes after the whole pattern, not inside it:
type ButtonProps = { label: string; disabled?: boolean }
function Button({ label, disabled = false }: ButtonProps) {
// …
}
{ label: string } inside a destructuring pattern would rename label to a variable called string. That’s JavaScript syntax, not a type.
Function types
Describe a function’s shape with an arrow-like type:
type Formatter = (value: number) => string
const toPounds: Formatter = (value) => `£${value.toFixed(2)}` // value: number, inferred
function formatAll(values: number[], format: Formatter) {
return values.map(format)
}
With an interface, use a call signature. This also lets the function have properties:
interface Counter {
(): number // callable
reset: () => void // and has a property
}
void vs undefined
A function type that returns void means “the return value is ignored”, not “returns nothing”. So a callback typed () => void may still return something:
const list: number[] = []
;[1, 2, 3].forEach((n) => list.push(n)) // fine: push returns a number, forEach ignores it
A function declaration with : void must not return a value. Use : undefined only when callers need the actual undefined.
never: functions that don’t return
function fail(message: string): never {
throw new Error(message)
}
After a call to a never function, TypeScript knows the code below it can’t run. That’s useful for narrowing:
function getPort(value: string | undefined) {
const port = value ?? fail('PORT is not set')
return Number(port) // port: string
}
Generics
A generic function works with many types while keeping the link between input and output. The type parameter (T) is like a function parameter, but for types.
function first<T>(items: T[]): T | undefined {
return items[0]
}
first([1, 2, 3]) // number | undefined
first(['a', 'b']) // string | undefined
Without generics you’d write any[] and lose the type, or write one function per type.
Constraints: extends
Limit what T can be, so you can use its properties:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
longest('apple', 'fig') // string
longest([1, 2], [1, 2, 3]) // number[]
keyof: a key of another parameter
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key])
}
const users = [{ name: 'Ada', age: 36 }]
pluck(users, 'name') // string[]
pluck(users, 'email') // error: "email" is not a key of the user
const type parameters
const keeps literal types instead of widening them (TypeScript 5.0+):
function routes<const T extends readonly string[]>(paths: T) {
return paths
}
routes(['/', '/about']) // readonly ['/', '/about'], not string[]
Generic arrow functions in .tsx
In a .tsx file, <T> before an arrow function looks like JSX. Add a comma:
const identity = <T,>(value: T) => value
Overloads
Overloads let one function have different signatures, when the return type depends on the arguments in a way a union can’t express. Write the public signatures first, then one implementation that handles them all:
function parse(value: string): number
function parse(value: string[]): number[]
function parse(value: string | string[]): number | number[] {
return Array.isArray(value) ? value.map(Number) : Number(value)
}
parse('42') // number
parse(['1', '2']) // number[]
The implementation signature is hidden: callers only see the overloads above it.
Try a union or a generic first. Overloads are only needed when the return type depends on which argument type came in, as above.
Typing this
Declare this as a fake first parameter. It’s removed from the output:
function onClick(this: HTMLButtonElement, event: MouseEvent) {
this.disabled = true
}
button.addEventListener('click', onClick)
Arrow functions don’t have their own this, so they can’t declare one.
Async functions
An async function always returns a Promise:
async function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`Failed: ${res.status}`)
return res.json()
}
res.json() returns Promise<any>, so the Promise<User> return type is what gives callers a real type. That’s a promise you’re making, not a check. See type guards for validating the data.
Type guards and assertion functions
Two special return types narrow the argument for the caller:
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') throw new Error('Not a string')
}
Full details in TypeScript type guards and narrowing.
Getting types from functions
Reuse a function’s types without writing them again:
function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age }
}
type NewUser = ReturnType<typeof createUser> // { id: string; name: string; age: number }
type CreateUserArgs = Parameters<typeof createUser> // [name: string, age: number]
type LoadedUser = Awaited<ReturnType<typeof getUser>> // unwraps the Promise
More utility types in the TypeScript note.
React event handlers
function Search() {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value)
}
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
}
return (
<form onSubmit={onSubmit}>
<input onChange={onChange} />
</form>
)
}
To pass a handler as a prop, reuse the element’s own type:
type Props = { onClick: React.ComponentProps<'button'>['onClick'] }
Or write the handler inline: TypeScript infers the event type from the JSX attribute.
Common interview questions
- What’s the difference between
unknownandany?anyturns checking off.unknownforces you to narrow before use. - What’s
never? The type with no values: a function that never returns, or a case that can’t happen (exhaustive checks). voidvsundefined?voidmeans the return value is ignored.undefinedis an actual value.- Why use generics? To keep the link between input and output types without
any. - When would you use overloads? When the return type depends on the argument type in a way a union or generic can’t express.
- What’s a type guard? A runtime check that narrows a type in that branch. See type guards.