Function Declarations and Expressions
The most fundamental distinction in TypeScript functions is between declarations and expressions. A function declaration uses the function keyword at the top level of your code, while a function expression assigns a function to a variable, property, or parameter.
// inferred return type
function sum(a: number, b: number) {
return a + b
}
// defined return type
function sum(a: number, b: number): number {
return a + b
}
Function expressions come in several forms. The most common are anonymous functions assigned to variables, arrow functions with block bodies, and arrow functions with implicit returns. You can also assign an arrow function to a variable and then call it normally.
// named function expression
const sum = function sum(a: number, b: number): number {
return a + b
}
// annonymous function expression
const sum = function (a: number, b: number): number {
return a + b
}
TypeScript gives you flexibility in how you handle types here. You can annotate the variable itself with a function type, and TypeScript will enforce that the assigned function matches. Alternatively, you can define a function type separately and reuse it:
// arrow function
const sum = (a: number, b: number): number => {
return a + b
}
// implicit return
const sum = (a: number, b: number): number => a + b
Type aliases and interfaces both work for defining these reusable function types. The key difference is that interfaces are extensible:
// implicit return of an object requires parentheses to disambiguate the curly braces
const sum = (a: number, b: number): { result: number } => ({ result: a + b })
const sum: (a: number, b: number) => number = (a, b) => a + b
The declare function syntax serves a different purpose entirely. It tells TypeScript "a variable with this name and type exists somewhere," which is why you'll see it primarily in .d.ts files when describing external libraries. You can extract the type from a declare function with the typeof operator.
type MathFn = (a: number, b: number) => number
const sum: MathFn = (a, b) => a + b
The most common source of confusion when writing or reading TypeScript functions is knowing when to use => versus : for return types. The rule of thumb is: => ReturnType is only used when defining a function type as a type in itself. In every other case, including function declarations and implementations, use : ReturnType.
Parameter Options: Optional, Default, and Rest
Optional parameters are marked with a ? after the parameter name. Order matters here: once you make one parameter optional, every parameter after it must also be optional. This is because a call like sum(1) is valid, but sum(, 2) is not. However, you can explicitly pass undefined to skip an optional parameter if you need to call a function with a later argument supplied.
type MathFn = {
(a: number, b: number): number
}
const sum: MathFn = (a, b) => a + b
Default parameters behave interestingly in TypeScript. When you assign a default value with =, TypeScript automatically treats that parameter as optional. So a default value effectively creates an optional parameter:
type MathFn = {
(a: number, b: number): number
operator: string
}
const sum: MathFn = (a, b) => a + b
sum.operator = '+'
This means you can have a required second parameter after an optional first one, thanks to the default value:
interface MathFn {
(a: number, b: number): number
operator: string
}
const sum: MathFn = (a, b) => a + b
sum.operator = '+'
When you extract the type from such a function though, you need to add | undefined manually. The default value = 0 is a JavaScript expression, not part of the type system.
Rest parameters collect all remaining arguments into an array. They must always be the last parameter in the signature, but can appear after any number of regular parameters.
declare function MathFn(a: number, b: number): number
declare namespace MathFn {
let operator: '+'
}
const sum: typeof MathFn = (a, b) => a + b
sum.operator = '+'
Functions in Objects and Classes
Within an object literal, a method is shorthand syntax for a function that is a property. A property can also hold a traditional function expression or an arrow function. The syntax choices mirror what you'd see in standalone function expressions:
const sum = (a: number, b?: number): number => a + (b ?? 0)
To extract the type of an object's method for reuse, you must type the enclosing object itself — you can't annotate the function directly inside the object literal. Similarly, adding extra properties to a function method inside an object literal isn't possible; you need to define the function separately first:
const sum = (a: number | undefined, b: number): number => (a ?? 0) + b
Classes offer two main ways to define functions. A regular method is the most common form. A class field, in contrast, ensures the function is bound to the specific instance of that class:
const sum = (a: number, b: number = 0): number => a + b
sum(1) // results in 1
sum(2, undefined) // results in 2
Extracting the type of a class method works, but interestingly, TypeScript still requires you to repeat the parameter and return types even when the method is meant to satisfy an interface:
const sum = (a: number, b: number | undefined = 0): number => a + b
One note: TypeScript's public, private, and protected modifiers are language features specific to TypeScript. JavaScript is moving toward native private field syntax via the class fields proposal.
Modules, Overloads, and More Advanced Patterns
Module declarations in .d.ts files work just like any other export. For a named export of the sum function, you declare it inside a declare module block. For a default export, use export default within that same block.
Function overloads let you define multiple call signatures for the same function. You declare each overload, then provide a single implementation whose parameter types must support all overloads:
const sum = (a: number = 0, b: number): number => a + b
sum(undefined, 3) // results in 3
Generators work in TypeScript with minimal annotation overhead. TypeScript correctly infers what iterator.next() returns. To get type safety on the value passed back into the generator via yield, add a type annotation to the variable that receives it.
type MathFn = (a: number | undefined, b: number) => number
const sum: MathFn = (a = 0, b) => a + b
Async functions differ from their JavaScript counterparts only in that the return type must be a Promise of the underlying value. Otherwise the syntax is identical:
const sum = (a: number = 0, ...rest: Array<number>): number => {
return rest.reduce((acc, n) => acc + n, a)
}
Generic functions written as declarations use the familiar <T> syntax. With arrow functions, the opening < is ambiguous in files that also support JSX — the compiler can't tell if it's a generic or JSX. Adding extends unknown disambiguates the syntax and conveniently demonstrates the extends constraint syntax:
type MathFn = (a?: number, ...rest: Array<number>) => number
const sum: MathFn = (a = 0, ...rest) => rest.reduce((acc, n) => acc + n, a)
Type Guards and Assertions
A common need is filtering falsy values out of an array. Plain JavaScript's filter won't narrow the element type in TypeScript — the result type stays Array<number | undefined>. A custom type guard function tells the compiler the predicate's semantics:
const math = {
sum(a: number, b: number): number {
return a + b
},
}
With that guard, the filter result is correctly narrowed to the non-falsy member type.
Assertion functions are a related mechanism. Runtime checks like throwing an error inside an if block are fine on their own, but extracting that logic into a function loses the type narrowing TypeScript had from the inline check. Marking the function with an asserts signature restores that behavior:
const math = {
sum: function sum(a: number, b: number): number {
return a + b
},
}
Both type guards and assertion functions are tools for turning runtime validation into compile-time narrowing. Which one you choose depends on whether the function's responsibility is testing a condition or asserting one.



