Functions Unpacked 0/7

JavaScript and TypeScriptFrom definition to callRead it, run it, reuse it

Functions
Unpacked

Parameters are the names a function promises to fill. Arguments are the values you fill them with. Start from that one idea and work through arrow functions, IIFEs, every way to invoke a function, typed parameters and rest arguments, ending with a pipeline you can run.

TypeScript 5ES2022
01

Params vs args

You will be able to point at any function and say which names are parameters and which values are arguments.

Core ideaTypeScriptJavaScriptMust know

A parameter is a name in the function definition. It is a placeholder the function promises to fill. An argument is the actual value you pass when you call the function. Think of a form: the labels on the form are parameters, and what you write in the boxes are arguments.

Parameterfunction greet(name) {}Declared once, in the definition.
Argumentgreet("Shree")Supplied every time you call it.
greet.tsTYPESCRIPT
// parameters: the names written in the definition
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

// arguments: the values you hand over at the call site
console.log(greet("Shree"));             // one argument, the default fills the rest
console.log(greet("Advik", "Namaste"));   // two arguments
console.log(greet.length);               // counts parameters before the first default

Why it matters: a default parameter only kicks in when the argument is missing or undefined, and fn.length tells you how many arguments a function expects up front.

Terminal OUTPUT
$ npx tsx greet.ts
Hello, Shree!
Namaste, Advik!
1

JavaScript never checks the count. Missing arguments arrive as undefined and extra ones are dropped. Objects are passed by sharing, so a function can change an object you gave it, but reassigning a parameter only changes its local name.

arity.jsJAVASCRIPT
function pair(a, b) {
  return [a, b];
}
console.log(pair(1));        // missing argument becomes undefined
console.log(pair(1, 2, 3));  // the extra argument is silently ignored

function rename(user, label) {
  user.name = label;   // mutates the object the caller also holds
  label = "changed";   // reassigning a parameter never reaches the caller
}
const account = { name: "old" };
let tag = "new";
rename(account, tag);
console.log(account.name, tag);

Watch for this: mutating an argument leaks out of the function, reassigning one does not. TypeScript catches the wrong argument count at compile time, plain JavaScript does not.

Terminal OUTPUT
$ node arity.js
[ 1, undefined ]
[ 1, 2 ]
new new
02

Kinds of functions

You will know the six everyday ways to write a function and when an arrow function is the right choice.

Arrow functionsHoistingthisMust know
kinds.tsTYPESCRIPT
// 1. declaration: hoisted, has its own this and arguments
function add(a: number, b: number) { return a + b; }

// 2. expression: a function stored in a variable
const sub = function (a: number, b: number) { return a - b; };

// 3. arrow: short, no own this, no arguments object
const mul = (a: number, b: number) => a * b;

// 4. method shorthand inside an object
const math = { div(a: number, b: number) { return a / b; } };

// 5. async and 6. generator
const later = async (x: number) => x * 10;
function* range(n: number) { for (let i = 0; i < n; i++) yield i; }

console.log(add(2, 3), sub(9, 4), mul(3, 3), math.div(8, 2));
later(4).then((v) => console.log(v, [...range(4)]));

Read it as: the body can be the same in every form. What changes is hoisting, how this is bound, and whether you can call it with new.

Terminal OUTPUT
$ npx tsx kinds.ts
5 5 9 4
40 [ 0, 1, 2, 3 ]
KindSyntaxHoistedOwn thisWorks with new
Declarationfunction f() {}YesYesYes
Expressionconst f = function () {}NoYesYes
Arrowconst f = () => {}NoNo borrows outerNo
Method{ f() {} }n/aYesNo
Asyncasync function f() {}as its formYesNo
Generatorfunction* f() {}as its formYesNo
Classclass F { constructor() {} }NoYesRequired

Arrow functions up close

Arrow functions drop the function keyword, return a single expression without return, and never create their own this or arguments. Wrap an object literal in parentheses, or the braces are read as a function body.

hoisting.jsJAVASCRIPT
console.log(area(3));   // works: declarations are hoisted
// console.log(cube(3)); // ReferenceError: Cannot access 'cube' before initialization

function area(r) { return Math.round(Math.PI * r * r); }
const cube = (n) => n ** 3;

// arrow bodies: implicit return, and parentheses around an object literal
const toUser = (id, name) => ({ id, name });
console.log(toUser(7, "Ravi"));

Gotcha: const and let functions sit in the temporal dead zone until their line runs, while declarations are usable from the top of the file.

Terminal OUTPUT
$ node hoisting.js
28
{ id: 7, name: 'Ravi' }
arrow-this.jsJAVASCRIPT
const timer = {
  ticks: 0,
  withArrow() {
    setTimeout(() => {
      this.ticks++;                 // arrow borrows this from withArrow
      console.log("arrow sees", this.ticks);
    }, 0);
  },
  withClassic() {
    setTimeout(function () {
      console.log("classic sees", this?.ticks); // this is no longer timer
    }, 0);
  },
};
timer.withArrow();
timer.withClassic();

Rule of thumb: use arrows for callbacks inside methods so this stays put, and use methods or declarations when the function itself needs a this.

Terminal OUTPUT
$ node arrow-this.js
arrow sees 1
classic sees undefined
03

Self invoked functions

You will be able to write an IIFE in every common shape and know the two jobs it still does well.

IIFEClosuresasyncJavaScript

An immediately invoked function expression, or IIFE, is defined and called in the same breath. The first pair of parentheses turns the function into an expression, and the second pair calls it, with any arguments you want to pass.

iife.jsJAVASCRIPT
// classic IIFE, with an argument passed straight in
(function (env) {
  console.log(`booting in ${env}`);
})("production");

// arrow IIFE
(() => console.log("arrow IIFE ran"))();

// module pattern: private state, public API
const ids = (() => {
  let next = 0;
  return { take: () => ++next, peek: () => next };
})();
console.log(ids.take(), ids.take(), ids.peek());
console.log(typeof next); // next never leaked

Why it matters: everything declared inside stays private. The returned object is the only door in, which is the classic module pattern that came before ES modules.

Terminal OUTPUT
$ node iife.js
booting in production
arrow IIFE ran
1 2 2
undefined
async-iife.tsTYPESCRIPT
const sleep = (ms: number) => new Promise<void>((done) => setTimeout(done, ms));

// async IIFE: await without top-level await support
(async () => {
  console.log("connecting...");
  await sleep(50);
  console.log("connected after 50ms");
})();

// a leading semicolon guards against the previous line
;(function () { console.log("safe even after a line with no semicolon"); })();

Order of output: the async IIFE pauses at await, so the next IIFE logs first. The leading semicolon protects you when the line above has none.

Terminal OUTPUT
$ npx tsx async-iife.ts
connecting...
safe even after a line with no semicolon
connected after 50ms
04

Calling functions

You will recognise every way a function gets invoked and predict what this will be in each.

Invocationcallapplybindnew

How you call a function decides what this is. The function body never changes, only the call site does.

PatternCall siteWhat this is
Function callfn(a)undefined in strict mode
Method callobj.fn(a)the object before the dot
callfn.call(ctx, a, b)ctx, arguments listed one by one
applyfn.apply(ctx, [a, b])ctx, arguments as one array
bindfn.bind(ctx, a)()ctx, fixed forever on the new function
Constructornew Fn(a)the brand new object
invoke.jsJAVASCRIPT
"use strict";
function whoAmI(greeting = "hi") {
  return `${greeting} from ${this?.name ?? "nobody"}`;
}
const svc = { name: "orders", whoAmI };

console.log(whoAmI());                                // function call
console.log(svc.whoAmI());                            // method call
console.log(whoAmI.call({ name: "billing" }, "hey")); // call: args one by one
console.log(whoAmI.apply({ name: "auth" }, ["yo"]));  // apply: args as an array
const bound = whoAmI.bind({ name: "search" }, "bound");
console.log(bound());                                 // bind: a new function, this fixed

Why it matters: bind can also lock in leading arguments, which is called partial application.

Terminal OUTPUT
$ node invoke.js
hi from nobody
hi from orders
hey from billing
yo from auth
bound from search
call-site.tsTYPESCRIPT
class Queue {
  constructor(public name: string, public size = 0) {}
}
const q = new Queue("emails");               // constructor invocation
console.log(q);

const double = (n: number) => n * 2;
console.log([1, 2, 3].map(double));          // pass the function, do not call it
// [1, 2, 3].map(double());                  // Error: expected 1 argument, got 0

const hooks: { onDone?: (ms: number) => void } = {};
hooks.onDone?.(120);                          // optional call: skipped, no crash
hooks.onDone = (ms) => console.log(`done in ${ms}ms`);
hooks.onDone?.(120);

Pass or call: map(double) hands the function over to be called later. map(double()) calls it right now. Optional call ?.() runs a function only if it exists.

Terminal OUTPUT
$ npx tsx call-site.ts
Queue { name: 'emails', size: 0 }
[ 2, 4, 6 ]
done in 120ms
05

Typed parameters

You will type parameters and return values in TypeScript, including optional, default, literal, generic and overloaded signatures.

TypeScriptOptional ?GenericsOverloadsMust know

TypeScript adds a type after each parameter with a colon. A ? makes it optional, a default value makes it optional and infers the type, and the return type goes after the closing parenthesis.

logger.tsTYPESCRIPT
type Level = "debug" | "info" | "error";

function log(message: string, level: Level = "info", meta?: Record<string, unknown>): void {
  const extra = meta ? ` ${JSON.stringify(meta)}` : "";
  console.log(`[${level.toUpperCase()}] ${message}${extra}`);
}

log("server started");                          // default level
log("cache miss", "debug", { key: "user:42" }); // optional meta supplied
// log("oops", "fatal");  // Error: '"fatal"' is not assignable to type 'Level'

Why it matters: a union of string literals turns a typo like "fatal" into a compile error instead of a production log you never find.

Terminal OUTPUT
$ npx tsx logger.ts
[INFO] server started
[DEBUG] cache miss {"key":"user:42"}
generics.tsTYPESCRIPT
// a function type alias with a generic payload
type Handler<T> = (payload: T) => void;
const onUser: Handler<{ id: number; name: string }> = (u) => console.log(`hello ${u.name}`);
onUser({ id: 1, name: "Shree" });

// generic parameter: the return type follows the argument
function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}
const n = first([10, 20]);    // n: number | undefined
const s = first(["a", "b"]);  // s: string | undefined
console.log(n, s);

Read it as: the generic T is a parameter for types. The argument you pass decides what T becomes, so the return type follows along for free.

Terminal OUTPUT
$ npx tsx generics.ts
hello Shree
10 a
overloads.tsTYPESCRIPT
// overloads: two call shapes, one implementation
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[] {
  return Array.isArray(input) ? input.map(Number) : Number(input);
}

const one = parse("42");          // one: number
const many = parse(["1", "2"]);   // many: number[]
console.log(one + 1, many);

When to reach for it: overloads describe distinct call shapes to the caller. The last, wider signature is the implementation and callers never see it.

Terminal OUTPUT
$ npx tsx overloads.ts
43 [ 1, 2 ]
06

Many arguments

You will collect any number of arguments with rest, spread arrays into calls and swap long parameter lists for one options object.

Rest ...argsSpreadargumentsTypeScript

Three dots do two opposite jobs. In a definition, ...args is a rest parameter that gathers the remaining arguments into a real array. At a call site, ...list is a spread that unpacks an array into separate arguments.

sum.jsJAVASCRIPT
// old way: arguments is array-like, not a real array
function oldSum() {
  return Array.prototype.reduce.call(arguments, (a, b) => a + b, 0);
}

// modern way: a rest parameter is a real array
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);

console.log(oldSum(1, 2, 3), sum(1, 2, 3, 4));
console.log(sum());               // zero arguments is fine, nums is []

Prefer rest: the old arguments object has no array methods and does not exist inside arrow functions. A rest parameter fixes both.

Terminal OUTPUT
$ node sum.js
6 10
0
rest.tsTYPESCRIPT
// fixed parameters first, the rest collects everything after
function tagAll(prefix: string, ...tags: string[]): string[] {
  return tags.map((t) => `${prefix}:${t}`);
}
console.log(tagAll("env", "dev", "qa", "prod"));

// tuple typed rest: a fixed shape with an optional last slot
function move(...[x, y, label]: [number, number, string?]): string {
  return `${label ?? "point"} -> (${x}, ${y})`;
}
console.log(move(3, 4), "|", move(0, 0, "origin"));

Typed rest: ...tags: string[] accepts any number of strings. A tuple type like [number, number, string?] gives each slot its own type and position.

Terminal OUTPUT
$ npx tsx rest.ts
[ 'env:dev', 'env:qa', 'env:prod' ]
point -> (3, 4) | origin -> (0, 0)
spread.tsTYPESCRIPT
// spread at the call site: an array becomes separate arguments
const scores = [71, 94, 88];
console.log(Math.max(...scores));

const coords: [number, number] = [3, 4];
console.log(Math.hypot(...coords));

// many settings? take one options object and destructure it
interface RequestOpts { url: string; retries?: number; timeoutMs?: number }
function request({ url, retries = 2, timeoutMs = 1000 }: RequestOpts): string {
  return `${url} retries=${retries} timeout=${timeoutMs}ms`;
}
console.log(request({ url: "/api/users", timeoutMs: 500 }));

Rule of thumb: past three parameters, take one options object. Callers name every argument, order stops mattering and defaults live in the destructuring.

Terminal OUTPUT
$ npx tsx spread.ts
94
5
/api/users retries=2 timeout=500ms
07

Full example

You will run a small text pipeline that uses rest parameters, generics, arrows, a higher order function and an async IIFE together.

Putting it togetherTypeScriptRunnable

This file turns article titles into URL slugs. Each step is an arrow function with a typed parameter, pipeline takes them all as a rest parameter, and an async IIFE runs the job. Save it, run it with Node 18 or newer, and you should see the output below.

pipeline.tsTYPESCRIPT
type Step<T> = (input: T) => T | Promise<T>;

// rest parameter: any number of typed steps
function pipeline<T>(...steps: Step<T>[]) {
  return async (input: T): Promise<T> => {
    let value = input;
    for (const step of steps) value = await step(value);
    return value;
  };
}

// arrow functions as small, typed steps
const trim: Step<string> = (s) => s.trim();
const lower: Step<string> = (s) => s.toLowerCase();
const slug: Step<string> = (s) => s.replace(/[^a-z0-9]+/g, "-");

// higher order: returns a step, with a default parameter
const withSuffix = (suffix = "-v1"): Step<string> => (s) => `${s}${suffix}`;

// async IIFE wires everything together
(async () => {
  const toSlug = pipeline(trim, lower, slug, withSuffix());
  const titles = ["  Function Args vs Params ", "Arrow Functions 101"];
  const slugs = await Promise.all(titles.map((t) => toSlug(t)));
  console.log(slugs);
  console.log(`steps used: ${[trim, lower, slug].length + 1}`);
})();

What each part teaches: ...steps is rest, Step<T> is a generic function type, withSuffix() returns a function with a default parameter, and titles.map((t) => toSlug(t)) passes one argument explicitly so map cannot sneak its index in as a second.

Terminal OUTPUT
$ npx tsx pipeline.ts
[ 'function-args-vs-params-v1', 'arrow-functions-101-v1' ]
steps used: 4
TerminalBASH
# no install needed, tsx runs TypeScript directly
$ npx tsx pipeline.ts
# prefer compiled output? type check, then run the JavaScript
$ npx tsc pipeline.ts --target es2022 --strict && node pipeline.js

Quick recall

Every term from this guide on one screen, for the next time you review a pull request.

TermWhat it isExample
ParameterA name in the definitionfunction f(x) {}
ArgumentA value at the call sitef(42)
Default parameterUsed when the argument is undefined(n = 10) => n
Optional parameterMay be left out, TypeScript only(id?: string) => id
Rest parameterGathers the remaining arguments(...ids: number[]) => ids
Spread argumentUnpacks an array into argumentsMath.max(...nums)
IIFEDefined and invoked at once(() => init())()
call, apply, bindInvoke with a chosen thisf.call(obj, a)
OverloadSeveral call signatures, one bodyfunction p(s: string): number;