Reading JavaScript Syntax
A field guide to the bits of modern JavaScript and TypeScript that confuse people the most, with the problem each one was built to solve.
Modern JavaScript has a lot of syntax. Most of it exists to solve a specific problem, and once you know the problem the syntax stops feeling arbitrary. Here is a tour of the pieces people stumble on most.
Destructuring
Destructuring pulls values out of objects and arrays by shape.
const user = { name: "Ada", role: "engineer" };
const { name, role = "reader" } = user;
console.log(name, role); // "Ada" "engineer"
It works on arrays too, and you can skip positions with a comma:
const [first, , third] = [1, 2, 3];
console.log(first, third); // 1 3
Rest and spread
Three dots ... do two opposite things depending on where they sit. On the receiving side, rest collects the remaining items into an array:
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
On the giving side, spread expands an iterable into individual arguments or elements:
const a = [1, 2];
const b = [3, 4];
const merged = [...a, ...b]; // [1, 2, 3, 4]
Optional chaining and nullish coalescing
These two exist to make "might be missing" code readable. Optional chaining ?. short-circuits when the left side is nullish:
const city = user?.address?.city ?? "Unknown";
The ?? returns the right side only when the left is null or undefined — not 0 or "", which || would have quietly swallowed.
Arrow functions and this
Arrow functions are shorter and inherit this from the surrounding scope, which makes them good for callbacks:
const doubled = nums.map((n) => n * 2);
They are a poor choice for object methods that need their own this. Use a regular function there.
TypeScript generics
A generic is a placeholder for a type. The classic identity function:
function identity<T>(value: T): T {
return value;
}
const n = identity(42); // number
const s = identity("hello"); // string
The <T> says: whatever type comes in, the same type goes out. Constraints narrow it:
function len<T extends { length: number }>(x: T): number {
return x.length;
}
async / await
await pauses an async function until a promise settles. It reads top-to-bottom like synchronous code:
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error("Failed");
return res.json();
}
Remember that await only waits inside a function marked async, and that a forgotten await quietly passes a promise along to whoever reads the return value.
A SQL aside
For contrast, a small query with a join and an aggregate:
SELECT author, COUNT(*) AS posts
FROM articles
WHERE published_at >= '2026-01-01'
GROUP BY author
ORDER BY posts DESC;
A bit of Rust
And a Rust snippet showing pattern matching on an enum:
enum Outcome<T, E> {
Ok(T),
Err(E),
}
fn describe(r: Outcome<i32, String>) -> String {
match r {
Ok(v) => format!("got {v}"),
Err(e) => format!("failed: {e}"),
}
}
The rule underneath
Every piece of syntax is a shorthand for a longer way of writing the same thing. When a line confuses you, ask what it expands to — ?. is a chain of if checks, ... is a loop, await is .then. The shorthand is the whole point, but the expansion is the explanation.
Comments(5)
The concentric radius tip alone was worth the read. I’ve been matching outer and inner radii for years without realizing why it felt off.
outer = inner + padding— that’s the whole rule.Same — once you see it, you can’t unsee the mismatch on nested cards.
Exactly. Glad this landed.
Also useful with Typeset measure caps — nesting and measure fight each other if you’re not careful.
Clear writing on a topic that usually gets hand-waved. Would love a follow-up on how this plays with nested dialogs.