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
00

Key terms in plain words

New to code? Start here. Every word below shows up later on this page, and each one comes with a kitchen comparison and a plain explanation. Underlined words in the modules link back to these cards.

23 terms in 5 groups. Hover an underlined word anywhere on the page for a quick reminder, or click it to jump here.

The basic idea

A function is a recipe card. You write it once, then cook from it whenever you like, with whatever ingredients you hand over that day.

Function
A named, reusable set of steps. You give it some values, it does its work, and it can hand one value back, called its return value.
For example greet("Shree") runs the steps in greet and returns "Hello, Shree!".
Parameter
A name written in the function's definition. It's a placeholder: it has no value of its own until someone calls the function and fills it in.
For example In function greet(name) {}, name is the parameter.
Argument
The real value you pass when you call a function. The same parameter gets a different argument on every call. JavaScript never checks how many you pass: missing ones arrive as undefined and extra ones are ignored.
For example In greet("Shree"), "Shree" is the argument.
Call (invoke)
Running a function by writing its name followed by brackets. Without the brackets you're only passing the recipe around, not cooking it.
For example greet is the recipe; greet() cooks it.
Default parameter
A fallback value written next to a parameter. It's used only when the argument is missing or undefined.
For example function greet(name, greeting = "Hello") {}
Mutate
To change an object's contents in place. A function receives the very same object you passed, not a copy, so mutating it shows up outside. Pointing the parameter at a new object only changes the function's own name for it.

Ways to write a function

The same recipe can be pinned on the wall, filed in a folder, scribbled on a sticky note or cooked on the spot.

Declaration and expression
A declaration starts with the word function and stands on its own line. An expression creates a function as a value, usually stored in a variable with const. They behave the same once created; the difference is when you can first use them.
For example function add() {} versus const add = function () {}
Hoisting
JavaScript sets up function declarations before any other line runs, so you can call one above where it's written. A function stored in a const can't be used until its line has run; before that it's in the "temporal dead zone".
Arrow function
A short way to write a function with =>. A single expression is returned without writing return, and it never gets its own this, so it borrows the one from the code around it.
For example const double = (n) => n * 2
Method
A function stored on an object and called through it, with a dot. Inside, this is the object before the dot.
For example math.div(8, 2) calls the div method of math.
async and await
An async function can pause at await while something slow happens, like a network request or a timer, without freezing the rest of the program. It always hands back a promise of its result.
IIFE
An immediately invoked function expression: a function defined and called in one go. Anything declared inside stays private, and only what it returns gets out, which is the old "module pattern".
For example (() => console.log("ran"))()

Calling and this

The recipe never changes. What changes is who asked for it, and that decides which table the dish goes to.

Call site and this
The call site is the line where a function gets called. In JavaScript, how you call a function decides what the special word this points to inside it: the object before the dot, a brand new object when you use new, or nothing at all for a plain call.
call, apply and bind
Three tools for choosing this yourself. call and apply run the function straight away (arguments one by one, or as an array). bind doesn't run it: it makes a new function with this, and optionally some arguments, fixed for good.
For example whoAmI.call({ name: "billing" }, "hey")
Callback
A function you hand to another function so it can call yours later, for example after a timer or once for each item in a list.
For example In nums.map((n) => n * 2), the arrow is the callback.

Any number of ingredients

What to do when you don't know how many things the cook will be handed.

Rest parameter
Three dots before the last parameter, ...nums, gather all the remaining arguments into a real array. It replaces the old arguments object, which isn't a true array and doesn't exist in arrow functions.
For example sum(1, 2, 3) gives nums the array [1, 2, 3].
Spread
The same three dots at a call site do the opposite job: they unpack an array into separate arguments.
For example Math.max(...[3, 9, 4]) is the same as Math.max(3, 9, 4).
Options object
Passing a single object instead of many separate arguments. Each value is named, the order stops mattering, and defaults sit right where you unpack it.
For example createUser({ name: "Ana", admin: true })

TypeScript extras

Labels on the ingredient jars, so the wrong thing can't go into the pot.

Type
A description of what a value may be, like text or a number. TypeScript lets you write one after each parameter and after the brackets for the return value, then flags any call that doesn't fit before the code runs.
For example function log(message: string): void {}
Optional parameter
A ? after a parameter name tells TypeScript the caller may leave it out. Inside the function it's then undefined, so you check for it before use.
For example function log(message: string, meta?: object) {}
Generic
A function with a parameter for types, usually called T. The argument you pass decides what T is, and the return type follows along.
For example first<T>(items: T[]): T returns a number from a list of numbers.
Overload
Several call signatures written above one function body, so each kind of input gets its own precise return type.
Higher order function
A function that accepts functions as arguments or returns a function. The full example chains small steps into one pipeline this way.
For example withSuffix("-v1") returns a new step function.
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;