Chapter 2Lesson 8
Quiz - Functions, naming, and control flow
You’re writing a helper that’s used by code earlier in the same module, and a separate type predicate isInvoice(x: unknown): x is Invoice. Which forms does a 2026 senior reach for?
Both as
const arrow expressions — arrow is the default, no exceptions.The earlier-used helper as a
function declaration (hoisting trigger); the type predicate as a function declaration (signature trigger).The earlier-used helper as an arrow; the type predicate as a
function expression assigned to const.Arrow
const is the default, but three triggers earn function: hoisting (so the binding is callable before its declaration line), named recursion, and TypeScript predicate/assertion signatures. The first trigger covers the early-use helper; the third covers the predicate — x is T is conventionally declared with function, and asserts x is T only parses on a function form.What does this snippet print?
const list = (page: number = 1, size: number = 20) => console.log(`p=${page} s=${size}`);
list(undefined, 0);list(null as unknown as number, 10);p=1 s=20 then p=1 s=10p=1 s=0 then p=null s=10p=1 s=0 then p=1 s=10Parameter defaults fire only on
undefined. The first call: page is undefined so the default 1 fires; size is 0 (falsy but defined) so the default does not fire and 0 flows through. The second call: null is not undefined, so the page default does not fire and null is interpolated; size is explicitly 10.Which of these names violate the “name for intent, not implementation” principle? Select all that apply.
customerArraypendingInvoicesnotDisableddatacustomerArray leaks the container — rename the type and the name lies. notDisabled is a negated boolean that compounds with ! at use sites into a double negative; name the positive condition (isEnabled). data is a vague abstraction that fits anything and communicates nothing. pendingInvoices is fine — concrete, intent-revealing, no container suffix.Under noFallthroughCasesInSwitch with the discriminated union type Payment = { status: 'pending' } | { status: 'paid' } | { status: 'failed' }, which switch compiles?
switch (p.status) { case 'pending': log('p'); case 'paid': return 'paid'; case 'failed': return 'failed'; default: return assertNever(p);}switch (p.status) { case 'pending': return 'p'; case 'paid': return 'paid'; default: return assertNever(p);}switch (p.status) { case 'pending': throw new Error('pending'); case 'paid': return 'paid'; case 'failed': return 'failed'; default: return assertNever(p);}The first fails:
case 'pending' runs log('p') without a terminator, so noFallthroughCasesInSwitch errors on the fallthrough. The second fails exhaustiveness: 'failed' has no case, so at default p is still { status: 'failed' }, not never, and assertNever(p) is rejected. The third compiles — throw is a valid terminator, every variant has a case, and p narrows to never at the default.A teammate writes const pageSize = input.pageSize || 20 and const city = user?.profile.address.city. Which fixes does a 2026 senior land in review? Select all that apply.
Change
|| to ?? so that a caller passing pageSize: 0 (meaning “show no rows”) isn’t silently swapped for 20.Add
?. at every nullable link — user?.profile?.address?.city — because each ?. only guards the one access to its left.Leave
|| alone; in TypeScript with strict mode, || and ?? behave identically.|| fires on every falsy value (0, '', false, plus nullish), so the pageSize: 0 case is swallowed; ?? fires only on nullish. Optional chaining short-circuits one link at a time — the original chain still throws at .address if profile is nullish. Strict mode does not change runtime operator semantics.After const { foo: bar } = obj, what’s true?
foo is now a local binding holding obj.bar.bar is now a local binding holding obj.foo. foo is not in scope.Both
foo and bar are in scope, both holding obj.foo.Destructure rename reads field-on-source
: local-binding. The source field is foo; the local binding the line creates is bar. This is the inverse direction from object-literal shorthand, which is why it’s the most mis-read piece of destructuring syntax — and why flipping it ships shadow bugs.What does the last line log?
let taxPercent = 20;
const makeTotal = () => { let calls = 0; return (cents: number) => { calls++; return { total: cents + (cents * taxPercent) / 100, calls }; };};
const total = makeTotal();total(10_000);taxPercent = 10;console.log(total(10_000));{ total: 12000, calls: 2 }{ total: 11000, calls: 2 }{ total: 11000, calls: 1 }Both halves come from the same rule: a closure holds a pointer to each binding and reads it when the body runs.
taxPercent is read at call time, so the reassignment to 10 between the two calls is visible — 11000, not the 12000 you’d get if the closure had snapshotted the value when makeTotal() returned. calls lives in the scope that one makeTotal() call created, and that scope outlives the call that made it, so the count carries over instead of resetting — the same mechanism that makes a counter factory’s state private.Quiz complete
Score by topic