Destructuring

Destructuring extracts values from arrays or fields from objects into named local variables in a single declaration. The RHS can be a variable, a function call, or a literal.

// ─── Array destructuring from a variable ───────────────────────────────────

const coords = [10, 20, 30, 40, 50]

const [x, y, z] = coords
console.log(x)   // 10
console.log(y)   // 20
console.log(z)   // 30

// Holes skip elements at that index.
const [, second, , fourth] = coords
console.log(second)  // 20
console.log(fourth)  // 40

// Swap via a temporary array.
let a: number = 1
let b: number = 2
const tmp = [b, a]
const [newA, newB] = tmp
console.log(newA)  // 2
console.log(newB)  // 1

// ─── Array destructuring from a literal ────────────────────────────────────

const [lo, , hi] = [0, 50, 100]
console.log(lo)  // 0
console.log(hi)  // 100

// ─── Array destructuring from a function call ───────────────────────────────

function range(start: number, n: number): number[] {
    let r = new Array<number>(n)
    for (let i = 0; i < n; i++) {
        r[i] = start + i
    }
    return r
}

const [first, second2, third] = range(5, 4)
console.log(first)    // 5
console.log(second2)  // 6
console.log(third)    // 7

// ─── Object destructuring from a variable ───────────────────────────────────

let pt: { x: number; y: number } = { x: 3, y: 7 }

// Rename on extract.
const { x: px, y: py } = pt
console.log(px)  // 3
console.log(py)  // 7

// Shorthand: local name matches field name.
let size: { width: number; height: number } = { width: 800, height: 600 }
const { width, height } = size
console.log(width)   // 800
console.log(height)  // 600

// String fields.
let person: { label: string; age: number } = { label: 'Alice', age: 30 }
const { label, age } = person
console.log(label)  // Alice
console.log(age)    // 30

// ─── Object destructuring from a literal ────────────────────────────────────

const { x: ox, y: oy } = { x: 9, y: 4 }
console.log(ox)  // 9
console.log(oy)  // 4

// ─── Object destructuring from a function call ───────────────────────────────

function makeRect(w: number, h: number): { width: number; height: number } {
    let r: { width: number; height: number } = { width: w, height: h }
    return r
}

const { width: rw, height: rh } = makeRect(1920, 1080)
console.log(rw)  // 1920
console.log(rh)  // 1080

// ─── Destructuring inside functions ─────────────────────────────────────────

function sumCoords(obj: { x: number; y: number }): number {
    const { x, y } = obj
    return x + y
}
function makePoint(px: number, py: number): { x: number; y: number } {
    let p: { x: number; y: number } = { x: px, y: py }
    return p
}
console.log(sumCoords(pt))               // 10
console.log(sumCoords(makePoint(4, 6)))  // 10

function dot(p: { x: number; y: number }, q: { x: number; y: number }): number {
    const { x: ax, y: ay } = p
    const { x: bx, y: by } = q
    return ax * bx + ay * by
}

let p1: { x: number; y: number } = { x: 1, y: 2 }
let p2: { x: number; y: number } = { x: 3, y: 4 }
console.log(dot(p1, p2))  // 11

function firstPlusLast(arr: number[]): number {
    const [head] = arr
    let tail: number = arr[arr.length - 1]
    return head + tail
}
console.log(firstPlusLast(coords))           // 60  (10 + 50)
console.log(firstPlusLast(range(1, 5)))      // 1 + 5 = 6

// ─── Destructured function parameters ───────────────────────────────────────
// Unlike a plain scalar parameter, a destructured one always needs an
// explicit type annotation — there's no sensible unannotated default for a
// pattern the way `number` is for a bare name.

interface Point { x: number; y: number }

function addPoints({ x, y }: Point, other: Point): number {
    return x + y + other.x + other.y
}
console.log(addPoints({ x: 1, y: 2 }, { x: 10, y: 20 }))  // 33

// Renaming works in parameter position too.
function describe({ x: px, y: py }: Point): string {
    return px + "," + py
}
console.log(describe({ x: 5, y: 6 }))  // 5,6

function sumFirstTwo([a, b]: number[]): number {
    return a + b
}
console.log(sumFirstTwo(coords))  // 30 (10 + 20)

// Holes work in a parameter pattern the same as in a destructuring statement.
function skipFirst([, b, c]: number[]): number {
    return b + c
}
console.log(skipFirst([100, 200, 300]))  // 500

// Arrow functions support object-destructured parameters too (array-
// destructured parameters don't, yet — array-typed closure parameters
// aren't supported at all independent of destructuring).
const area = ({ x, y }: Point): number => x * y
console.log(area({ x: 5, y: 6 }))  // 30

// Class methods and constructors support destructured parameters as well.
class Vec {
    sum: number
    constructor({ x, y }: Point) {
        this.sum = x + y
    }
    static add({ x, y }: Point, other: Point): number {
        return x + y + other.x + other.y
    }
}
const v = new Vec({ x: 3, y: 4 })
console.log(v.sum)                                  // 7
console.log(Vec.add({ x: 1, y: 1 }, { x: 2, y: 2 })) // 6

// ─── Destructuring default values (`[a = expr]`, `{ a = expr }`) ───────────
// An array pattern's default fires exactly when that position is past the
// source array's actual length — a shorter array is ordinary, valid JS, not
// an error.

const [dp1 = 100, dp2 = 200, dp3 = 300] = [1, 2]
console.log(dp1)  // 1   (present, default unused)
console.log(dp2)  // 2   (present, default unused)
console.log(dp3)  // 300 (absent, default used)

// A later default may reference an earlier binding in the same pattern.
const [base = 5, offset = base] = [10]
console.log(base)    // 10 (present)
console.log(offset)  // 10 (absent, defaults to the just-bound `base`)

// An object pattern's default only works on a nullable *reference* field
// (`T | null` where T is string/array/object/class) — the one field shape
// this compiler can reliably tell "not provided" apart from a real value
// for. A nullable *scalar* field (`number | null`) represents its null as
// an in-band 0/false sentinel indistinguishable from a legitimate zero, so
// a default there — and on any non-nullable field — is a compile-time
// rejection instead of a silent wrong answer.

interface Settings { label: string | null }
const withLabel: Settings = { label: "custom" }
const withoutLabel: Settings = { label: null }

const { label: l1 = "default" } = withLabel
const { label: l2 = "default" } = withoutLabel
console.log(l1)  // custom
console.log(l2)  // default

// Destructured function parameters support defaults too, on both kinds of
// pattern, the same way.
function firstTwoOrDefaults([a = -1, b = -1]: number[]): string {
    return a + "," + b
}
console.log(firstTwoOrDefaults([7]))     // 7,-1
console.log(firstTwoOrDefaults([7, 8]))  // 7,8

// ─── Destructuring assignment (`[a, b] = expr`, `({ a, b } = expr)`) ───────
// Assigns into already-declared variables, rather than declaring new ones.
// V1 scope: every target must be a plain variable — no nested patterns, no
// rest, no per-element default (the declaration form's own richer defaults
// don't extend to assignment yet).

let assignA: number = 0
let assignB: number = 0
;[assignA, assignB] = [1, 2]
console.log(assignA)  // 1
console.log(assignB)  // 2

// The classic swap idiom, without a temporary variable this time.
;[assignA, assignB] = [assignB, assignA]
console.log(assignA)  // 2
console.log(assignB)  // 1

// Object form needs parens at statement level — `{` would otherwise start
// a block, same restriction real JS has.
interface Coord { cx: number; cy: number }
let targetX: number = 0
let targetY: number = 0
const coord: Coord = { cx: 5, cy: 6 }
;({ cx: targetX, cy: targetY } = coord)
console.log(targetX)  // 5
console.log(targetY)  // 6

// A source array shorter than the pattern reads zero for the missing
// positions — same as the declaration form's own out-of-bounds behavior,
// not a real JS `undefined`.
;[assignA, assignB] = [9]
console.log(assignA)  // 9
console.log(assignB)  // 0

// ─── Array rest destructuring (`[a, ...rest]`) ──────────────────────────────
// Collects every remaining position into a real, independent new array —
// works in a declaration, a destructured function parameter, and a
// destructuring assignment alike.

const [restFirst, ...restTail] = [1, 2, 3]
console.log(restFirst)      // 1
console.log(restTail.length) // 2
console.log(restTail[0])    // 2
console.log(restTail[1])    // 3

// Shorter than the pattern: rest is just empty, not an error.
const [rf2, rf3, ...restEmpty] = [1, 2]
console.log(rf2)               // 1
console.log(rf3)               // 2
console.log(restEmpty.length)  // 0

function firstAndRestCount([head, ...tail]: number[]): number {
    return head + tail.length
}
console.log(firstAndRestCount([10, 20, 30, 40]))  // 13

// Destructuring assignment form: the rest target must already be
// declared as an array.
let swapFirst: number = 0
let swapRest: number[] = []
;[swapFirst, ...swapRest] = [5, 6, 7]
console.log(swapFirst)       // 5
console.log(swapRest.length) // 2

// A rest array is a real copy, not a view into the source — mutating the
// source afterward doesn't affect it.
const restSource: number[] = [1, 2, 3]
const [, ...restCopy] = restSource
restSource.push(99)
console.log(restCopy.length)  // 2

// --- Destructuring the loop variable of a for...of (TDD-00065 Stage 1) ---

// Object pattern: each element's fields become loop-body bindings.
interface Vertex { x: number; y: number }
const points: Vertex[] = [{ x: 1, y: 2 }, { x: 3, y: 4 }]
let coordSum = 0
for (const { x, y } of points) {
    coordSum = coordSum + x + y
}
console.log(coordSum)  // 10

// Renaming works the same as in a declaration pattern.
for (const { x: px, y: py } of points) {
    console.log(px + '/' + py)  // 1/2 then 3/4
}

// Array pattern over an array-of-arrays, with a hole and a rest element.
const rows: number[][] = [[1, 2, 3], [4, 5, 6]]
for (const [first, , ...others] of rows) {
    console.log(first + ':' + others.length)  // 1:1 then 4:1
}

// --- Nested patterns (TDD-00065 Stage 2) ---

// Array inside array.
const matrix: number[][] = [[1, 2], [3, 4]]
const [[m00, m01], [m10, m11]] = matrix
console.log(m00 + ' ' + m01 + ' ' + m10 + ' ' + m11)  // 1 2 3 4

// Object inside object, and an array inside an object.
interface Leaf { v: number }
interface Tree { left: Leaf; span: number[] }
const tree: Tree = { left: { v: 9 }, span: [100, 200] }
const { left: { v: leafVal }, span: [lo, hi] } = tree
console.log(leafVal + ' ' + lo + ' ' + hi)  // 9 100 200

// A nested pattern also works as a for-of loop variable and a parameter.
const cells: number[][] = [[1, 2], [3, 4]]
for (const [lft, rgt] of cells) {
    console.log(lft * rgt)  // 2 then 12
}
function sumPair([[p, q]]: number[][]): number { return p + q }
console.log(sumPair([[8, 9]]))  // 17

// --- String / numeric-literal keys (TDD-00065 Stage 3a) ---

// A string-literal key is equivalent to the identifier key of the same name;
// it always binds through an explicit `: local` (no shorthand for a
// non-identifier key).
interface Sized { width: number; height: number }
const sizedBox: Sized = { width: 20, height: 10 }
const { "width": bw, "height": bh } = sizedBox
console.log(bw + 'x' + bh)  // 20x10

// A field whose name isn't a valid identifier is reachable only this way.
const named = { "first-name": 'Ada', "last-name": 'Lovelace' }
const { "first-name": firstName, "last-name": lastName } = named
console.log(firstName + ' ' + lastName)  // Ada Lovelace

// Numeric-literal keys, and a string key as a for-of loop variable.
const scores = [{ 1: 'gold', 2: 'silver' }, { 1: 'first', 2: 'second' }]
for (const { 1: primary } of scores) {
    console.log(primary)  // gold then first
}

// --- Object rest `{ ...rest }` (TDD-00065 Stage 3b) ---

// The rest binding collects every source field not named earlier into a fresh,
// first-class object (usable for field access, a further spread, or JSON).
interface Account { id: number; name: string; balance: number }
const account: Account = { id: 42, name: 'Ada', balance: 100 }
const { id: accountId, ...accountRest } = account
console.log(accountId)                       // 42
console.log(accountRest.name + ' ' + accountRest.balance)  // Ada 100
console.log(JSON.stringify(accountRest))     // {"name":"Ada","balance":100}

// Rest works as a for-of loop variable and a destructured parameter too.
const accounts: Account[] = [account, { id: 7, name: 'Alan', balance: 50 }]
for (const { id, ...info } of accounts) {
    console.log(id + ':' + info.name)        // 42:Ada then 7:Alan
}
function describeAccount({ name, ...numbers }: Account): string {
    return name + ' #' + numbers.id
}
console.log(describeAccount(account))        // Ada #42
View source on GitHubexamples/basics/destructuring.ts