Control Flow: given
switch has three footguns. given is the same idea with none of them — and a different
shape, so you can see at a glance which one you are reading.
/#
## The three problems with `switch`
switch (kind) {
case 'cat':
case 'dog':
label = 'pet' // no break — falls through!
case 'lion':
label = 'wild'
}
1. **Fallthrough by default.** Forget `break` and the next arm runs too. The
common case needs the extra keyword; the rare case gets the short spelling.
2. **One shared scope.** Every arm declares into the same block, so
`let x` in two arms is a redeclaration error.
3. **It compares with `===`.** So a boxed `new String('cat')` misses, exactly
the footgun TJS's `==` exists to fix.
## `given` fixes all three, and looks different
given kind {
'cat', 'dog' { … } // several values on one arm
'lion' { … }
} else { … }
No `case`. No colons. No implicit blocks. `else` for the remaining arm.
Each arm has **its own scope**, there is **no fallthrough**, and matching uses
**`==`** (honest equality — unwraps boxed primitives, no coercion).
## Why a new word instead of fixing `switch`
Because a silent change of meaning is the worst outcome. Measured against a
model asked what the code does:
switch, C syntax 0/5 correct, 5 CONFIDENTLY WRONG
switch, new syntax 0/5 correct, 0 wrong, 5 no-answer
given, new syntax 4/5 correct, 0 wrong
Changing the shape eliminated every confident wrong answer — which is the
failure mode that ships bugs. `switch` still means exactly what C means, and
warns, pointing here. See `docs/case-study-switch.md`.
#/
// --- Several values per arm, no fallthrough ---
function describe(kind: '') {
given kind {
'cat', 'dog' { return 'pet' }
'lion', 'tiger' { return 'wild' }
} else {
return 'unknown'
}
}
console.log('describe:')
console.log(' cat ->', describe('cat'))
console.log(' dog ->', describe('dog'))
console.log(' tiger ->', describe('tiger'))
console.log(' emu ->', describe('emu'))
// --- Each arm has its OWN scope ---
// In a `switch`, both arms would be declaring `label` into one shared block
// and the second `let` would be a redeclaration error.
function greet(lang: '') {
given lang {
'en' {
let label = 'Hello'
return label + '!'
}
'fr' {
let label = 'Bonjour'
return label + ' !'
}
} else {
return '...'
}
}
console.log('\nper-arm scope:')
console.log(' en ->', greet('en'))
console.log(' fr ->', greet('fr'))
// --- Matching is `==`, so a boxed primitive still matches ---
// `switch` compares with `===`, where this falls through to the default.
//
// Note it is matched against a LOCAL value here. Passing `new String('cat')` to
// `describe` above would never reach the `given` at all: the parameter is declared
// `kind: ''`, so validation rejects the object first. Two safety nets, and the outer
// one fires first — which is the point of typing the parameter.
function boxedStillMatches() {
const kind = new String('cat')
given kind {
'cat', 'dog' { return 'pet' }
} else {
return 'unknown'
}
}
console.log('\nboxed primitive:')
console.log(' new String("cat") ->', boxedStillMatches())
test 'several values share one arm' {
expect(describe('cat')).toBe('pet')
expect(describe('dog')).toBe('pet')
}
test 'no fallthrough — the next arm does not run' {
expect(describe('lion')).toBe('wild')
}
test 'else catches the rest' {
expect(describe('emu')).toBe('unknown')
}
test 'each arm has its own scope' {
expect(greet('en')).toBe('Hello!')
expect(greet('fr')).toBe('Bonjour !')
}
test 'matches with == so boxed primitives work' {
// The `switch`/`===` version returns 'unknown' here.
expect(boxedStillMatches()).toBe('pet')
}