Skip to main content

TS Ep 12: Function Typing, Call Signatures, and Void Semantics

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 12: This Article
Functions are the fundamental building blocks of JavaScript applications. In TypeScript, typing functions involves parameter annotations, return types, generic parameters, call signatures, and understanding the subtle semantics of void vs undefined.

1. Function Annotations & Syntax
#

TypeScript allows annotating function declarations, arrow functions, and method expressions:

// Named Function Declaration
function add(a: number, b: number): number {
  return a + b;
}

// Arrow Function Expression
const multiply = (a: number, b: number): number => a * b;

// Function Type Alias
type MathFn = (a: number, b: number) => number;

const divide: MathFn = (a, b) => a / b;

2. Optional, Default, and Rest Parameters
#

Optional Parameters (?)
#

Optional parameters must always appear after required parameters in the parameter list. Inside the function body, an optional parameter x?: number has the type number | undefined.

function formatLog(message: string, userId?: string): string {
  if (userId) {
    return `[User: ${userId}]: ${message}`;
  }
  return `[System]: ${message}`;
}

Default Parameters
#

When you supply a default value, TypeScript automatically infers the parameter’s type and treats it as optional for the caller:

// 'prefix' is inferred as 'string', default value "LOG"
function printLog(message: string, prefix = "LOG") {
  console.log(`[${prefix.toUpperCase()}]: ${message}`);
}

printLog("Server started");          // Output: [LOG]: Server started
printLog("DB connected", "DEBUG");   // Output: [DEBUG]: DB connected

Rest Parameters (...args)
#

Rest parameters capture an arbitrary number of arguments into an array. They must be typed as an Array type (T[] or Tuple):

function sumAll(initialValue: number, ...values: number[]): number {
  return values.reduce((acc, curr) => acc + curr, initialValue);
}

console.log(sumAll(10, 1, 2, 3, 4)); // 20

3. Destructured Parameter Typing
#

A common source of syntax confusion is typing destructured object parameters.

// ❌ WRONG SYNTAX: This attempts to rename 'name' to 'string' in JavaScript!
// function printUser({ name: string, age: number }) { ... }

// 🟢 CORRECT SYNTAX: Destructure on left, type object on right!
function printUser({ name, age }: { name: string; age: number }) {
  console.log(`${name} is ${age} years old.`);
}

// 🟢 BETTER: Use a type alias for clean signatures
type UserPayload = {
  name: string;
  age: number;
};

function printUserClean({ name, age }: UserPayload) {
  console.log(`${name} is ${age} years old.`);
}

4. The Quirks of the void Return Type
#

In traditional languages (C, C++, Java), void means a function returns nothing and cannot return a value.

In TypeScript, void behaves differently depending on whether it is used on a direct function implementation or a callback signature.

Case 1: Direct Function Implementation
#

When a function implementation is annotated with : void, it cannot return a value (other than undefined or an empty return; statement).

function logToConsole(msg: string): void {
  console.log(msg);
  // return "Done"; // ❌ Compiler Error: Type 'string' is not assignable to type 'void'.
  return; // Valid
}

Case 2: Callback Signatures (The Ignored Return Value Rule!)
#

When void is used as the return type of a callback parameter or function type alias, TypeScript permits the implementation to return ANY value, but guarantees that callers will ignore the return value!

type VoidCallback = () => void;

// 🟢 WORKED: Returning a boolean is allowed for a void callback!
const onClick: VoidCallback = () => {
  return true; // No compiler error!
};

// Why does this exist?
// It allows passing array methods like Array.prototype.push directly to callbacks!
const numbers = [1, 2, 3];
const targetStorage: number[] = [];

// Array.prototype.forEach expects a callback: (val: number) => void
// targetStorage.push() RETURNS a number (the new array length).
// Because of the 'void' callback rule, TS permits this cleanly:
numbers.forEach((n) => targetStorage.push(n));

void vs undefined
#

Return TypeMeaningPermitted Implementations
void (Callback)“I will ignore whatever you return”return true;, return 42;, return;
void (Direct)“This function produces no return value”return; or no return statement
undefined“This function MUST return literal undefinedreturn undefined; (MUST explicitly return!)
// ❌ Error: A function whose declared type is 'undefined' MUST return a value.
// function getNothing(): undefined {
//   console.log("nothing");
// }

function getNothingCorrect(): undefined {
  console.log("nothing");
  return undefined; // Must be explicit!
}

5. Call Signatures & Callable Objects
#

In JavaScript, functions are first-class objects. A function can have properties attached to it (like express() or axios).

To type a function that also has properties attached to it, use an Object Type with a Call Signature:

// A callable function that also has a 'version' string property attached!
interface ExecutableRunner {
  (command: string): boolean; // The Call Signature
  version: string;            // Property attached to function
  description: string;        // Property attached to function
}

// Implementation
const runner: ExecutableRunner = Object.assign(
  (command: string) => {
    console.log(`Executing ${command}`);
    return true;
  },
  {
    version: "2.1.0",
    description: "Production Runner CLI",
  }
);

// Calling as a function:
runner("deploy"); // Output: "Executing deploy"

// Accessing properties:
console.log(runner.version);     // Output: "2.1.0"
console.log(runner.description); // Output: "Production Runner CLI"

Summary & Next Steps
#

In this episode:

  • We annotated functions, arrow functions, and parameter signatures.
  • We structured destructured parameter signatures correctly ({ name }: User).
  • We mastered void semantics: Direct functions cannot return values, but void callbacks ignore returned values to support JavaScript patterns like forEach.
  • We differentiated void from undefined.
  • We built Callable Objects using interface Call Signatures.

In Episode 13: Function Overloads, we will learn how to declare multiple function signatures for functions that behave differently based on their argument types!

typescript - This article is part of a series.
Part 12: This Article