diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,131 @@
 
 ---
 
+## [0.17.5] - 2026-08-19
+
+Point release on the 0.17.x line. Two language-surface changes lead: sum-type eliminators unify on
+`match` (scrutinee-first), and the primitive set is tentatively finalized — `strings.lines`/`unlines`
+are removed in favour of `hydra.formatting` term helpers. A symlink-aware filesystem family lands
+across all ten hosts, and Hydra's own build system continues its migration into Hydra.
+
+**Backward-incompatible.** The `cases` -> `match` rename and the `strings.lines`/`unlines` removal
+both change the published kernel surface. Code written against 0.17.4 that uses `Optionals.cases`
+or `strings.lines`/`strings.unlines` must be updated.
+
+### Highlights
+
+- **Sum-type eliminators unify on `match`** ([#615](https://github.com/CategoricalData/hydra/issues/615)):
+  the `cases`/`match` helper pair is flipped to a single scrutinee-first `match` convention across the
+  Haskell, Java, Python, and Scala DSLs, the `optionals.cases` -> `optionals.match` kernel primitive,
+  and the corresponding implementations in TypeScript and all four Lisp dialects.
+- **Primitive set tentatively finalized** ([#417](https://github.com/CategoricalData/hydra/issues/417)):
+  `strings.lines` and `strings.unlines` are removed as primitives; their semantics are preserved as
+  `hydra.formatting.lines`/`unlines` term helpers, with overlay implementations dropped across all ten
+  hosts and callers repointed.
+- **Symlink-aware filesystem primitives** ([#666](https://github.com/CategoricalData/hydra/issues/666)):
+  `status(followLinks)`, `createSymlink`, and `readSymlink` land with native implementations for every
+  host — Haskell, Java, Python, Scala, TypeScript, Clojure, Common Lisp, Scheme, and Emacs Lisp.
+- **Build system promotion continues** ([#416](https://github.com/CategoricalData/hydra/issues/416)):
+  adds `hydra.build.walk` extension-glob helpers (`extensionOf`/`filterByExtension`/`matchesExtension`)
+  and `hydra.build.comparereportlogic`, a set of pure snapshot-compare decision helpers.
+
+### Bug fixes
+
+- **`math.range` was inclusive of both bounds** ([#647](https://github.com/CategoricalData/hydra/issues/647)),
+  silently fabricating an extra element in index arithmetic.
+- **Java coder TCO-`cases` codegen** dropped enclosing-method type parameters from union-variant casts
+  (found via [#666](https://github.com/CategoricalData/hydra/issues/666)).
+- **`ShaclRdf` promoted to a DSL module** ([#652](https://github.com/CategoricalData/hydra/issues/652))
+  so the SHACL pipeline works outside Haskell.
+- **Cold-seed shim portability**: the R22 bootstrap shim used GNU-only `\+` and `\b`, which fail under
+  BSD `sed` on macOS ([#417](https://github.com/CategoricalData/hydra/issues/417)).
+- **Bootstrap Haskell target missing `unix`**: the demo's static `package.yaml` was not updated when
+  `System.Posix.Files` entered the kernel overlay, breaking every `*-to-haskell` bootstrap cell
+  ([#670](https://github.com/CategoricalData/hydra/issues/670)).
+- **Bootstrap comparison scored Scala and TypeScript cells as passing without comparing anything**:
+  `compare_output`'s target switch was missing both languages, so the baseline path was malformed,
+  every file counted as "no baseline found", and a zero-file comparison reported `pass`. A Scala or
+  TypeScript *target* cell could never fail ([#671](https://github.com/CategoricalData/hydra/issues/671)).
+
+### Known issues
+
+- A pre-#630 post-generation text pass rewrites `hydra.lib.<sub>` -> `hydra.overlay.<lang>.lib.<sub>`
+  in generated **doc comments** as well as code, because its quote-prefix guard does not recognize
+  mid-sentence prose references. Affects two kernel doc strings in `src/main`
+  (`hydra.error.system`, `hydra.error.packaging`) plus ~20 generated docstrings in the test tree;
+  cosmetic only, no behavioral impact. Tracked as part of
+  [#633](https://github.com/CategoricalData/hydra/issues/633).
+- Bootstrap comparison diff detail is not persisted to the run directory
+  ([#671](https://github.com/CategoricalData/hydra/issues/671)).
+
+---
+
+## [0.17.4] - 2026-08-09
+
+Point release on the 0.17.x line. Fixes an inference bug that mis-typed generated Java and Python for
+`readFile`, adds the `divide` primitive, and restores `hydra-ext` to the published package set. Release
+validation also surfaced a family of pre-existing cross-host code-generation defects — none of which the
+per-host CI jobs can catch, because only the bootstrap exercises one host generating *another*.
+
+### Highlights
+
+- **Output-only nominal generalization fixed** ([#637](https://github.com/CategoricalData/hydra/issues/637),
+  with [#638](https://github.com/CategoricalData/hydra/issues/638) /
+  [#639](https://github.com/CategoricalData/hydra/issues/639) /
+  [#640](https://github.com/CategoricalData/hydra/issues/640)): a nominal type appearing only in the
+  *output* position of a primitive's signature was generalized to a free type variable instead of resolving
+  as nominal, mis-typing the generated Java and Python for `readFile`. Also widens universe closure and adds
+  a `kernelTypeUniverse` helper, an effect DSL builder, and `effects`/`files`/`text`/`system` lib wrappers.
+- **`divide` primitive** ([#317](https://github.com/CategoricalData/hydra/issues/317)): now present in the
+  published kernel. This also clears the cold-clone dangle, since a cold seed against published 0.17.4
+  resolves `DefMath.divide`.
+- **`hydra-ext` republished** ([#636](https://github.com/CategoricalData/hydra/issues/636)): back in the
+  published set after its absence from 0.17.1–0.17.3, shipping to Hackage and PyPI.
+
+### Bug fixes
+
+- **Overlay-lib redirection skipped for the Haskell target**
+  ([#630](https://github.com/CategoricalData/hydra/issues/630)):
+  the Java, Python, Scheme, Clojure and Common Lisp heads resolved the overlay lib directory using an
+  all-lowercase path and did not lower-case the names they scanned. Haskell's overlay tree is capitalized
+  (`Hydra/Overlay/Haskell/Lib/`), so every `hydra.lib.*` redirect silently failed and the generated Haskell
+  imported `Hydra.Lib.*` instead of `Hydra.Overlay.Haskell.Lib.*`, producing hundreds of type errors. The
+  defect was invisible on case-sensitive filesystems, where the failed directory check falls back to a
+  hard-coded list that happens to be correct. The Python head carried *two* independent copies of this scan;
+  only the one in `generation.py` feeds the Haskell coder.
+- **Lisp-as-host bootstrap** ([#520](https://github.com/CategoricalData/hydra/issues/520),
+  [#630](https://github.com/CategoricalData/hydra/issues/630)): the four Lisp head decode drivers called
+  `from_json` with a stale 4-argument signature, and the Lisp bootstrap-driver coders were not pre-bound with
+  `overlaySubs`.
+- **Python-as-host dropped the `Graph` argument** ([#630](https://github.com/CategoricalData/hydra/issues/630)):
+  `overlaySubs` was not threaded into the Python head's `write_*` wrappers.
+- **Clojure `resolve-coder` and `decimal-to-float32`**
+  ([#630](https://github.com/CategoricalData/hydra/issues/630)): a stray parenthesis closed a `case` early,
+  making an unsupported-default block run unconditionally; and `(float (.doubleValue ...))` threw on
+  `Float.MAX_VALUE`, now saturating via `(.floatValue (bigdec x))`.
+- **Cold-seeder failed on macOS** ([#630](https://github.com/CategoricalData/hydra/issues/630)): the #376
+  cold seeder used a bare `sed -i -e`, which is GNU-only and aborts immediately under BSD `sed`.
+
+### Release engineering
+
+- **Review checkpoint before Maven Central publishes**
+  ([#591](https://github.com/CategoricalData/hydra/issues/591)): `HYDRA_JAVA_PUBLISH_HOLD=1` uploads the
+  aggregated Java deployment as `USER_MANAGED`, so it validates and then holds for review instead of
+  publishing automatically. This mirrors `HYDRA_SCALA_PUBLISH_HOLD` and makes an upload reversible — a held
+  deployment can be dropped, whereas an automatic one cannot be undone.
+
+### Known issues
+
+- **`hydra-ext` is not published to Maven Central**
+  ([#643](https://github.com/CategoricalData/hydra/issues/643)): it does not compile for the Java target,
+  because the visitor-pattern inner interface the Java coder emits collides with the enclosing `Visitor`
+  type in `hydra.cpp.syntax`. Only Java is affected; the Python coder emits sibling classes rather than
+  nested ones. ext ships to Hackage and PyPI at 0.17.4 and is expected to rejoin the Java set once fixed.
+- **TypeScript no longer self-hosts**
+  ([#642](https://github.com/CategoricalData/hydra/issues/642)): the TypeScript host's JSON decoder fails to
+  decode most of the kernel, so `typescript-as-host` code generation is broken. No published artifact is
+  affected — `dist/typescript` is generated by the Haskell host and passes its test suite.
+
 ## [0.17.3] - 2026-08-01
 
 Point release on the 0.17.x line, focused on **release-artifact integrity**. The 0.17.2 release shipped
diff --git a/hydra-typescript.cabal b/hydra-typescript.cabal
--- a/hydra-typescript.cabal
+++ b/hydra-typescript.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.39.1.
+-- This file has been generated from package.yaml by hpack version 0.38.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           hydra-typescript
-version:        0.17.4
+version:        0.17.5
 synopsis:       Hydra's TypeScript coder: emit TypeScript source from Hydra modules
 description:    Hydra is an implementation of the LambdaGraph data model, which takes advantage of an isomorphism between labeled hypergraphs and typed lambda calculus: in Hydra, "graphs are programs, and programs are graphs". TypeScript support for Hydra
 category:       Data
@@ -28,6 +28,7 @@
 library
   exposed-modules:
       Hydra.Dsl.TypeScript.Syntax
+      Hydra.Overlay.Haskell.Dsl.TypeScript.Helpers
       Hydra.TypeScript.Coder
       Hydra.TypeScript.Language
       Hydra.TypeScript.Operators
@@ -40,6 +41,6 @@
   build-depends:
       base >=4.19.0 && <4.22
     , containers >=0.6.7 && <0.8
-    , hydra-kernel ==0.17.4
+    , hydra-kernel ==0.17.5
     , scientific >=0.3.7 && <0.4
   default-language: Haskell2010
diff --git a/src/main/haskell/Hydra/Dsl/TypeScript/Syntax.hs b/src/main/haskell/Hydra/Dsl/TypeScript/Syntax.hs
--- a/src/main/haskell/Hydra/Dsl/TypeScript/Syntax.hs
+++ b/src/main/haskell/Hydra/Dsl/TypeScript/Syntax.hs
@@ -7,7 +7,7 @@
 import qualified Hydra.Core as Core
 import qualified Hydra.TypeScript.Syntax as Syntax
 import qualified Hydra.Typed as Typed
-import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
+import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, lines, map, pure, sum, unlines)
 import qualified Data.Scientific as Sci
 import qualified Data.Int as I
 
diff --git a/src/main/haskell/Hydra/Overlay/Haskell/Dsl/TypeScript/Helpers.hs b/src/main/haskell/Hydra/Overlay/Haskell/Dsl/TypeScript/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/main/haskell/Hydra/Overlay/Haskell/Dsl/TypeScript/Helpers.hs
@@ -0,0 +1,623 @@
+-- | A DSL for constructing TypeScript syntax trees in Haskell.
+module Hydra.Overlay.Haskell.Dsl.TypeScript.Helpers where
+
+import Hydra.TypeScript.Syntax
+import qualified Data.Int as I
+
+
+-- * Identifiers
+
+-- | Create an identifier
+-- Example: ident "foo"
+ident :: String -> Identifier
+ident = Identifier
+
+-- | Create a qualified name from a list of strings
+-- Example: qualifiedName ["module", "submodule", "name"]
+qualifiedName :: [String] -> QualifiedName
+qualifiedName = fmap Identifier
+
+
+-- * Literals
+
+-- | Create a string literal with double quotes
+-- Example: stringLit "hello"
+stringLit :: String -> Literal
+stringLit s = LiteralString $ StringLiteral s False
+
+-- | Create a string literal with single quotes
+-- Example: stringLitSingle "hello"
+stringLitSingle :: String -> Literal
+stringLitSingle s = LiteralString $ StringLiteral s True
+
+-- | Create an integer literal
+-- Example: intLit 42
+intLit :: I.Int64 -> Literal
+intLit = LiteralNumber . NumericLiteralInteger
+
+-- | Create a floating-point literal
+-- Example: floatLit 3.14
+floatLit :: Double -> Literal
+floatLit = LiteralNumber . NumericLiteralFloat
+
+-- | Create a boolean literal
+-- Example: boolLit True
+boolLit :: Bool -> Literal
+boolLit = LiteralBoolean
+
+-- | The null literal
+nullLit :: Literal
+nullLit = LiteralNull
+
+-- | The undefined literal
+undefinedLit :: Literal
+undefinedLit = LiteralUndefined
+
+-- | Create a BigInt literal
+-- Example: bigIntLit 123
+bigIntLit :: Integer -> Literal
+bigIntLit = LiteralBigInt
+
+
+-- * Expressions
+
+-- | Create an identifier expression
+-- Example: identExpr "foo"
+identExpr :: String -> Expression
+identExpr = ExpressionIdentifier . ident
+
+-- | Create a literal expression
+-- Example: litExpr (stringLit "hello")
+litExpr :: Literal -> Expression
+litExpr = ExpressionLiteral
+
+-- | Create a string expression
+-- Example: stringExpr "hello"
+stringExpr :: String -> Expression
+stringExpr = litExpr . stringLit
+
+-- | Create an integer expression
+-- Example: intExpr 42
+intExpr :: I.Int64 -> Expression
+intExpr = litExpr . intLit
+
+-- | Create a floating-point expression
+-- Example: floatExpr 3.14
+floatExpr :: Double -> Expression
+floatExpr = litExpr . floatLit
+
+-- | Create a boolean expression
+-- Example: boolExpr True
+boolExpr :: Bool -> Expression
+boolExpr = litExpr . boolLit
+
+-- | The null expression
+nullExpr :: Expression
+nullExpr = litExpr nullLit
+
+-- | The undefined expression
+undefinedExpr :: Expression
+undefinedExpr = litExpr undefinedLit
+
+-- | Create an array expression
+-- Example: arrayExpr [intExpr 1, intExpr 2, intExpr 3]
+arrayExpr :: [Expression] -> Expression
+arrayExpr = ExpressionArray . fmap ArrayElementExpression
+
+-- | Create an object expression from key-value pairs
+-- Example: objectExpr [("name", stringExpr "John"), ("age", intExpr 30)]
+objectExpr :: [(String, Expression)] -> Expression
+objectExpr pairs = ExpressionObject $ fmap toProperty pairs
+  where
+    toProperty (k, v) = Property (identExpr k) v PropertyKindInit False False
+
+-- | Create a function call expression
+-- Example: callExpr (identExpr "foo") [intExpr 1, intExpr 2]
+callExpr :: Expression -> [Expression] -> Expression
+callExpr callee args = ExpressionCall $ CallExpression callee args False
+
+-- | Create an optional chaining call expression
+-- Example: optionalCallExpr (identExpr "foo") [intExpr 1]
+optionalCallExpr :: Expression -> [Expression] -> Expression
+optionalCallExpr callee args = ExpressionCall $ CallExpression callee args True
+
+-- | Create a member access expression (obj.prop)
+-- Example: memberExpr (identExpr "obj") "prop"
+memberExpr :: Expression -> String -> Expression
+memberExpr obj prop = ExpressionMember $ MemberExpression obj (identExpr prop) False False
+
+-- | Create a computed member access expression (obj[expr])
+-- Example: computedMemberExpr (identExpr "arr") (intExpr 0)
+computedMemberExpr :: Expression -> Expression -> Expression
+computedMemberExpr obj prop = ExpressionMember $ MemberExpression obj prop True False
+
+-- | Create an optional chaining member expression (obj?.prop)
+-- Example: optionalMemberExpr (identExpr "obj") "prop"
+optionalMemberExpr :: Expression -> String -> Expression
+optionalMemberExpr obj prop = ExpressionMember $ MemberExpression obj (identExpr prop) False True
+
+-- | Create a conditional (ternary) expression
+-- Example: condExpr (identExpr "x") (intExpr 1) (intExpr 0)
+condExpr :: Expression -> Expression -> Expression -> Expression
+condExpr test consequent alternate = ExpressionConditional $
+  ConditionalExpression test consequent alternate
+
+-- | Create a binary expression
+-- Example: binaryExpr BinaryOperatorAdd (intExpr 1) (intExpr 2)
+binaryExpr :: BinaryOperator -> Expression -> Expression -> Expression
+binaryExpr op left right = ExpressionBinary $ BinaryExpression op left right
+
+-- | Create an addition expression
+-- Example: addExpr (intExpr 1) (intExpr 2)
+addExpr :: Expression -> Expression -> Expression
+addExpr = binaryExpr BinaryOperatorAdd
+
+-- | Create a subtraction expression
+-- Example: subExpr (intExpr 5) (intExpr 3)
+subExpr :: Expression -> Expression -> Expression
+subExpr = binaryExpr BinaryOperatorSubtract
+
+-- | Create a multiplication expression
+-- Example: mulExpr (intExpr 3) (intExpr 4)
+mulExpr :: Expression -> Expression -> Expression
+mulExpr = binaryExpr BinaryOperatorMultiply
+
+-- | Create a division expression
+-- Example: divExpr (intExpr 10) (intExpr 2)
+divExpr :: Expression -> Expression -> Expression
+divExpr = binaryExpr BinaryOperatorDivide
+
+-- | Create a strict equality expression (===)
+-- Example: eqExpr (identExpr "x") (intExpr 1)
+eqExpr :: Expression -> Expression -> Expression
+eqExpr = binaryExpr BinaryOperatorStrictEqual
+
+-- | Create a strict inequality expression (!==)
+-- Example: neqExpr (identExpr "x") (intExpr 1)
+neqExpr :: Expression -> Expression -> Expression
+neqExpr = binaryExpr BinaryOperatorStrictNotEqual
+
+-- | Create a logical AND expression
+-- Example: andExpr (identExpr "a") (identExpr "b")
+andExpr :: Expression -> Expression -> Expression
+andExpr = binaryExpr BinaryOperatorAnd
+
+-- | Create a logical OR expression
+-- Example: orExpr (identExpr "a") (identExpr "b")
+orExpr :: Expression -> Expression -> Expression
+orExpr = binaryExpr BinaryOperatorOr
+
+-- | Create a unary expression
+-- Example: unaryExpr UnaryOperatorNot (identExpr "x")
+unaryExpr :: UnaryOperator -> Expression -> Expression
+unaryExpr op arg = ExpressionUnary $ UnaryExpression op arg True
+
+-- | Create a logical NOT expression
+-- Example: notExpr (identExpr "x")
+notExpr :: Expression -> Expression
+notExpr = unaryExpr UnaryOperatorNot
+
+-- | Create a negation expression
+-- Example: negateExpr (identExpr "x")
+negateExpr :: Expression -> Expression
+negateExpr = unaryExpr UnaryOperatorNegate
+
+-- | Create an assignment expression
+-- Example: assignExpr (identPattern "x") (intExpr 42)
+assignExpr :: Pattern -> Expression -> Expression
+assignExpr left right = ExpressionAssignment $
+  AssignmentExpression AssignmentOperatorAssign left right
+
+-- | The 'this' expression
+thisExpr :: Expression
+thisExpr = ExpressionThis
+
+-- | Create a 'new' expression
+-- Example: newExpr (identExpr "Date") []
+newExpr :: Expression -> [Expression] -> Expression
+newExpr callee args = ExpressionNew $ CallExpression callee args False
+
+-- | Create an await expression
+-- Example: awaitExpr (callExpr (identExpr "fetch") [stringExpr "url"])
+awaitExpr :: Expression -> Expression
+awaitExpr = ExpressionAwait
+
+-- | Create a yield expression
+-- Example: yieldExpr (Just (intExpr 1))
+yieldExpr :: Maybe Expression -> Expression
+yieldExpr = ExpressionYield
+
+-- | Create a spread expression
+-- Example: spreadExpr (identExpr "arr")
+spreadExpr :: Expression -> Expression
+spreadExpr = ExpressionSpread . SpreadElement
+
+-- | Create a parenthesized expression
+-- Example: parenExpr (addExpr (intExpr 1) (intExpr 2))
+parenExpr :: Expression -> Expression
+parenExpr = ExpressionParenthesized
+
+
+-- * Arrow Functions
+
+-- | Create an arrow function with an expression body
+-- Example: arrowExpr ["x", "y"] (addExpr (identExpr "x") (identExpr "y"))
+arrowExpr :: [String] -> Expression -> Expression
+arrowExpr params body = ExpressionArrow $ ArrowFunctionExpression
+  (fmap identPattern params)
+  (ArrowFunctionBodyExpression body)
+  False
+
+-- | Create an async arrow function with an expression body
+-- Example: asyncArrowExpr ["x"] (awaitExpr (callExpr (identExpr "fetch") [identExpr "x"]))
+asyncArrowExpr :: [String] -> Expression -> Expression
+asyncArrowExpr params body = ExpressionArrow $ ArrowFunctionExpression
+  (fmap identPattern params)
+  (ArrowFunctionBodyExpression body)
+  True
+
+-- | Create an arrow function with a block body
+-- Example: arrowBlockExpr ["x"] [returnStmt (Just (identExpr "x"))]
+arrowBlockExpr :: [String] -> [Statement] -> Expression
+arrowBlockExpr params body = ExpressionArrow $ ArrowFunctionExpression
+  (fmap identPattern params)
+  (ArrowFunctionBodyBlock body)
+  False
+
+
+-- * Patterns
+
+-- | Create an identifier pattern
+-- Example: identPattern "x"
+identPattern :: String -> Pattern
+identPattern = PatternIdentifier . ident
+
+-- | Create an object destructuring pattern
+-- Example: objectPattern [identPattern "a", identPattern "b"]
+objectPattern :: [Pattern] -> Pattern
+objectPattern ps = PatternObject $ ObjectPattern $ fmap toProperty ps
+  where
+    toProperty p = case p of
+      PatternIdentifier i -> ObjectPatternPropertyProperty $
+        Property (ExpressionIdentifier i) (ExpressionIdentifier i) PropertyKindInit False True
+      _ -> ObjectPatternPropertyProperty $
+        Property nullExpr (patternToExpr p) PropertyKindInit False False
+    patternToExpr (PatternIdentifier i) = ExpressionIdentifier i
+    patternToExpr _ = nullExpr
+
+-- | Create an array destructuring pattern
+-- Example: arrayPattern [Just (identPattern "a"), Nothing, Just (identPattern "b")]
+arrayPattern :: [Maybe Pattern] -> Pattern
+arrayPattern = PatternArray
+
+-- | Create a rest pattern
+-- Example: restPattern "args"
+restPattern :: String -> Pattern
+restPattern = PatternRest . RestElement . identPattern
+
+-- | Create a pattern with a default value
+-- Example: defaultPattern "x" (intExpr 0)
+defaultPattern :: String -> Expression -> Pattern
+defaultPattern name def = PatternAssignment $
+  AssignmentPattern (identPattern name) def
+
+
+-- * Statements
+
+-- | Create an expression statement
+-- Example: exprStmt (callExpr (identExpr "console.log") [stringExpr "hello"])
+exprStmt :: Expression -> Statement
+exprStmt = StatementExpression
+
+-- | Create a block statement
+-- Example: blockStmt [exprStmt (intExpr 1), exprStmt (intExpr 2)]
+blockStmt :: [Statement] -> Statement
+blockStmt = StatementBlock
+
+-- | Create a return statement
+-- Example: returnStmt (Just (intExpr 42))
+returnStmt :: Maybe Expression -> Statement
+returnStmt = StatementReturn
+
+-- | Create a throw statement
+-- Example: throwStmt (newExpr (identExpr "Error") [stringExpr "oops"])
+throwStmt :: Expression -> Statement
+throwStmt = StatementThrow . ThrowStatement
+
+-- | Create a break statement
+-- Example: breakStmt Nothing
+breakStmt :: Maybe String -> Statement
+breakStmt = StatementBreak . fmap ident
+
+-- | Create a continue statement
+-- Example: continueStmt Nothing
+continueStmt :: Maybe String -> Statement
+continueStmt = StatementContinue . fmap ident
+
+-- | Create an if statement
+-- Example: ifStmt (identExpr "x") (returnStmt (Just (intExpr 1))) Nothing
+ifStmt :: Expression -> Statement -> Maybe Statement -> Statement
+ifStmt test consequent alternate = StatementIf $ IfStatement test consequent alternate
+
+-- | Create a while statement
+-- Example: whileStmt (boolExpr True) (blockStmt [breakStmt Nothing])
+whileStmt :: Expression -> Statement -> Statement
+whileStmt test body = StatementWhile $ WhileStatement test body
+
+-- | Create a for statement
+-- Example: forStmt (Just (ForInitVariable (constDecl "i" (intExpr 0)))) (Just (binaryExpr BinaryOperatorLessThan (identExpr "i") (intExpr 10))) (Just (unaryExpr UnaryOperatorIncrement (identExpr "i"))) (blockStmt [])
+forStmt :: Maybe ForInit -> Maybe Expression -> Maybe Expression -> Statement -> Statement
+forStmt init test update body = StatementFor $ ForStatement init test update body
+
+-- | Create a for-of statement
+-- Example: forOfStmt False (ForInLeftVariable (constDecl "item" Nothing)) (identExpr "items") (blockStmt [])
+forOfStmt :: Bool -> ForInLeft -> Expression -> Statement -> Statement
+forOfStmt await left right body = StatementForOf $ ForOfStatement await left right body
+
+-- | Create a try statement
+-- Example: tryStmt [exprStmt (callExpr (identExpr "riskyOp") [])] (Just (catchClause (Just (identPattern "e")) [throwStmt (identExpr "e")])) Nothing
+tryStmt :: [Statement] -> Maybe CatchClause -> Maybe [Statement] -> Statement
+tryStmt block handler finalizer = StatementTry $ TryStatement block handler finalizer
+
+-- | Create a catch clause
+-- Example: catchClause (Just (identPattern "e")) [throwStmt (identExpr "e")]
+catchClause :: Maybe Pattern -> [Statement] -> CatchClause
+catchClause = CatchClause
+
+-- | Create a switch statement
+-- Example: switchStmt (identExpr "x") [switchCase (Just (intExpr 1)) [returnStmt (Just (stringExpr "one"))], switchCase Nothing [returnStmt (Just (stringExpr "default"))]]
+switchStmt :: Expression -> [SwitchCase] -> Statement
+switchStmt discriminant cases = StatementSwitch $ SwitchStatement discriminant cases
+
+-- | Create a switch case
+-- Example: switchCase (Just (intExpr 1)) [returnStmt (Just (stringExpr "one"))]
+switchCase :: Maybe Expression -> [Statement] -> SwitchCase
+switchCase = SwitchCase
+
+-- | An empty statement
+emptyStmt :: Statement
+emptyStmt = StatementEmpty
+
+-- | A debugger statement
+debuggerStmt :: Statement
+debuggerStmt = StatementDebugger
+
+
+-- * Variable Declarations
+
+-- | Create a const declaration
+-- Example: constDecl "x" (Just (intExpr 42))
+constDecl :: String -> Maybe Expression -> VariableDeclaration
+constDecl name init = VariableDeclaration VariableKindConst
+  [VariableDeclarator (identPattern name) init]
+
+-- | Create a let declaration
+-- Example: letDecl "x" (Just (intExpr 42))
+letDecl :: String -> Maybe Expression -> VariableDeclaration
+letDecl name init = VariableDeclaration VariableKindLet
+  [VariableDeclarator (identPattern name) init]
+
+-- | Create a var declaration
+-- Example: varDecl "x" (Just (intExpr 42))
+varDecl :: String -> Maybe Expression -> VariableDeclaration
+varDecl name init = VariableDeclaration VariableKindVar
+  [VariableDeclarator (identPattern name) init]
+
+-- | Create a variable declaration statement
+-- Example: varDeclStmt (constDecl "x" (Just (intExpr 42)))
+varDeclStmt :: VariableDeclaration -> Statement
+varDeclStmt = StatementVariableDeclaration
+
+
+-- * Function Declarations
+
+-- | Create a function declaration
+-- Example: funcDecl "add" ["x", "y"] [returnStmt (Just (addExpr (identExpr "x") (identExpr "y")))]
+funcDecl :: String -> [String] -> [Statement] -> FunctionDeclaration
+funcDecl name params body = FunctionDeclaration
+  (ident name)
+  (fmap identPattern params)
+  body
+  False
+  False
+
+-- | Create an async function declaration
+-- Example: asyncFuncDecl "fetchData" ["url"] [returnStmt (Just (awaitExpr (callExpr (identExpr "fetch") [identExpr "url"])))]
+asyncFuncDecl :: String -> [String] -> [Statement] -> FunctionDeclaration
+asyncFuncDecl name params body = FunctionDeclaration
+  (ident name)
+  (fmap identPattern params)
+  body
+  True
+  False
+
+-- | Create a function declaration statement
+-- Example: funcDeclStmt (funcDecl "add" ["x", "y"] [returnStmt (Just (addExpr (identExpr "x") (identExpr "y")))])
+funcDeclStmt :: FunctionDeclaration -> Statement
+funcDeclStmt = StatementFunctionDeclaration
+
+
+-- * Class Declarations
+
+-- | Create a class declaration
+-- Example: classDecl "Person" Nothing [methodDef "greet" [] [returnStmt (Just (stringExpr "hello"))]]
+classDecl :: String -> Maybe Expression -> [MethodDefinition] -> ClassDeclaration
+classDecl name superClass body = ClassDeclaration (ident name) superClass body
+
+-- | Create a method definition
+-- Example: methodDef "greet" [] [returnStmt (Just (stringExpr "hello"))]
+methodDef :: String -> [String] -> [Statement] -> MethodDefinition
+methodDef name params body = MethodDefinition
+  (identExpr name)
+  (FunctionExpression Nothing (fmap identPattern params) body False False)
+  MethodKindMethod
+  False
+  False
+
+-- | Create a constructor definition
+-- Example: constructorDef ["name"] [exprStmt (assignExpr (PatternIdentifier (ident "this.name")) (identExpr "name"))]
+constructorDef :: [String] -> [Statement] -> MethodDefinition
+constructorDef params body = MethodDefinition
+  (identExpr "constructor")
+  (FunctionExpression Nothing (fmap identPattern params) body False False)
+  MethodKindConstructor
+  False
+  False
+
+-- | Create a static method definition
+-- Example: staticMethodDef "create" [] [returnStmt (Just (newExpr (identExpr "Person") []))]
+staticMethodDef :: String -> [String] -> [Statement] -> MethodDefinition
+staticMethodDef name params body = MethodDefinition
+  (identExpr name)
+  (FunctionExpression Nothing (fmap identPattern params) body False False)
+  MethodKindMethod
+  False
+  True
+
+-- | Create a class declaration statement
+-- Example: classDeclStmt (classDecl "Person" Nothing [])
+classDeclStmt :: ClassDeclaration -> Statement
+classDeclStmt = StatementClassDeclaration
+
+
+-- * Module Items
+
+-- | Create a statement module item
+-- Example: stmtItem (varDeclStmt (constDecl "x" (Just (intExpr 42))))
+stmtItem :: Statement -> ModuleItem
+stmtItem = ModuleItemStatement
+
+-- | Create an import declaration
+-- Example: importDecl [importNamed "foo" "foo"] "./module.js"
+importDecl :: [ImportClause] -> String -> ImportDeclaration
+importDecl specifiers source = ImportDeclaration specifiers (StringLiteral source False)
+
+-- | Create a named import
+-- Example: importNamed "foo" "bar"  -- import { foo as bar }
+importNamed :: String -> String -> ImportClause
+importNamed imported local = ImportClauseNamed $
+  ImportSpecifier (ident imported) (ident local)
+
+-- | Create a default import
+-- Example: importDefault "foo"  -- import foo from ...
+importDefault :: String -> ImportClause
+importDefault = ImportClauseDefault . ImportDefaultSpecifier . ident
+
+-- | Create a namespace import
+-- Example: importNamespace "foo"  -- import * as foo from ...
+importNamespace :: String -> ImportClause
+importNamespace = ImportClauseNamespace . ImportNamespaceSpecifier . ident
+
+-- | Create an import module item
+-- Example: importItem (importDecl [importDefault "React"] "react")
+importItem :: ImportDeclaration -> ModuleItem
+importItem = ModuleItemImport
+
+-- | Create a named export declaration
+-- Example: namedExportDecl [("foo", "foo")] Nothing
+namedExportDecl :: [(String, String)] -> Maybe String -> ExportDeclaration
+namedExportDecl specifiers source = ExportDeclarationNamed $ NamedExport
+  (fmap (\(local, exported) -> ExportSpecifier (ident local) (ident exported)) specifiers)
+  (fmap (\s -> StringLiteral s False) source)
+
+-- | Create a default export declaration
+-- Example: defaultExportDecl (identExpr "foo")
+defaultExportDecl :: Expression -> ExportDeclaration
+defaultExportDecl = ExportDeclarationDefault
+
+-- | Create an export declaration for a statement
+-- Example: exportDeclStmt (varDeclStmt (constDecl "x" (Just (intExpr 42))))
+exportDeclStmt :: Statement -> ExportDeclaration
+exportDeclStmt = ExportDeclarationDeclaration
+
+-- | Create an export module item
+-- Example: exportItem (defaultExportDecl (identExpr "foo"))
+exportItem :: ExportDeclaration -> ModuleItem
+exportItem = ModuleItemExport
+
+
+-- * Programs
+
+-- | Create a module program
+-- Example: moduleProgram [importItem (importDecl [importDefault "foo"] "./foo.js"), stmtItem (varDeclStmt (constDecl "x" (Just (intExpr 42))))]
+moduleProgram :: [ModuleItem] -> Program
+moduleProgram body = Program body SourceTypeModule
+
+-- | Create a script program
+-- Example: scriptProgram [stmtItem (varDeclStmt (constDecl "x" (Just (intExpr 42))))]
+scriptProgram :: [ModuleItem] -> Program
+scriptProgram body = Program body SourceTypeScript
+
+
+-- * Type Expressions
+
+-- | Create an identifier type expression
+-- Example: identType "string"
+identType :: String -> TypeExpression
+identType = TypeExpressionIdentifier . ident
+
+-- | Create an array type expression
+-- Example: arrayType (identType "number")
+arrayType :: TypeExpression -> TypeExpression
+arrayType = TypeExpressionArray . ArrayTypeExpression
+
+-- | Create a union type expression
+-- Example: unionType [identType "string", identType "number"]
+unionType :: [TypeExpression] -> TypeExpression
+unionType = TypeExpressionUnion
+
+-- | Create an optional type expression
+-- Example: optionalType (identType "string")
+optionalType :: TypeExpression -> TypeExpression
+optionalType = TypeExpressionOptional
+
+-- | Create a parameterized type expression
+-- Example: paramType (identType "Array") [identType "string"]
+paramType :: TypeExpression -> [TypeExpression] -> TypeExpression
+paramType base args = TypeExpressionParameterized $
+  ParameterizedTypeExpression base args
+
+-- | The 'any' type
+anyType :: TypeExpression
+anyType = TypeExpressionAny
+
+-- | The 'void' type
+voidType :: TypeExpression
+voidType = TypeExpressionVoid
+
+-- | The 'never' type
+neverType :: TypeExpression
+neverType = TypeExpressionNever
+
+
+-- * Comments and Documentation
+
+-- | Create a line comment
+-- Example: lineComment "This is a comment"
+lineComment :: String -> Comment
+lineComment = CommentLine
+
+-- | Create a block comment
+-- Example: blockComment "This is a\nmultiline comment"
+blockComment :: String -> Comment
+blockComment = CommentBlock
+
+-- | Create a documentation comment
+-- Example: docComment "Description of the function" [paramTag "x" (Just (identType "number")) "The input value"]
+docComment :: String -> [DocumentationTag] -> Comment
+docComment desc tags = CommentDocumentation $ DocumentationComment desc tags
+
+-- | Create a @param documentation tag
+-- Example: paramTag "x" (Just (identType "number")) "The input value"
+paramTag :: String -> Maybe TypeExpression -> String -> DocumentationTag
+paramTag name typ desc = DocumentationTag "param" typ (Just $ ident name) desc
+
+-- | Create a @returns documentation tag
+-- Example: returnsTag (Just (identType "number")) "The result"
+returnsTag :: Maybe TypeExpression -> String -> DocumentationTag
+returnsTag typ desc = DocumentationTag "returns" typ Nothing desc
+
+-- | Create a @type documentation tag
+-- Example: typeTag (identType "number")
+typeTag :: TypeExpression -> DocumentationTag
+typeTag typ = DocumentationTag "type" (Just typ) Nothing ""
diff --git a/src/main/haskell/Hydra/TypeScript/Coder.hs b/src/main/haskell/Hydra/TypeScript/Coder.hs
--- a/src/main/haskell/Hydra/TypeScript/Coder.hs
+++ b/src/main/haskell/Hydra/TypeScript/Coder.hs
@@ -61,7 +61,7 @@
 import qualified Hydra.Validation as Validation
 import qualified Hydra.Variables as Variables
 import qualified Hydra.Variants as Variants
-import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
+import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, lines, map, pure, sum, unlines)
 import qualified Data.Scientific as Sci
 import qualified Data.Map as M
 import qualified Data.Set as S
@@ -93,10 +93,10 @@
       let subs = Rewriting.subterms term
           ownVars =
                   case (Strip.deannotateTerm term) of
-                    Core.TermLambda v0 -> Optionals.cases (Core.lambdaDomain v0) Sets.empty (\d -> Variables.freeVariablesInType d)
+                    Core.TermLambda v0 -> Optionals.match (Core.lambdaDomain v0) Sets.empty (\d -> Variables.freeVariablesInType d)
                     Core.TermTypeApplication v0 -> Variables.freeVariablesInType (Core.typeApplicationTermType v0)
                     Core.TermTypeLambda _ -> Sets.empty
-                    Core.TermLet v0 -> Lists.foldl (\acc -> \b -> Optionals.cases (Core.bindingTypeScheme b) acc (\ts -> Sets.union acc (Variables.freeVariablesInType (Core.typeSchemeBody ts)))) Sets.empty (Core.letBindings v0)
+                    Core.TermLet v0 -> Lists.foldl (\acc -> \b -> Optionals.match (Core.bindingTypeScheme b) acc (\ts -> Sets.union acc (Variables.freeVariablesInType (Core.typeSchemeBody ts)))) Sets.empty (Core.letBindings v0)
                     _ -> Sets.empty
           childVars = Lists.foldl (\acc -> \s -> Sets.union acc (collectInnerTypeImports currentNs s)) Sets.empty subs
       in (filterNonLocalNames currentNs (Sets.union ownVars childVars))
@@ -237,11 +237,11 @@
       Core.TermVariable v0 ->
         let local = Formatting.sanitizeWithUnderscores Language.typeScriptReservedWords (Names.localNameOf v0)
             varExpr =
-                    Optionals.cases (Names.moduleNameOf v0) (tsExprIdent local) (\ns -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName ns)) (tsExprIdent local) (
+                    Optionals.match (Names.moduleNameOf v0) (tsExprIdent local) (\ns -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName ns)) (tsExprIdent local) (
                       let nsSegs = Lists.drop 1 (Strings.splitOn "." (Packaging.unModuleName ns))
                           alias = Strings.concat2 "$mod_" (Strings.join "_" nsSegs)
                       in (tsMember (tsExprIdent alias) local)))
-        in (Optionals.cases (Lexical.lookupPrimitive g v0) varExpr (\prim ->
+        in (Optionals.match (Lexical.lookupPrimitive g v0) varExpr (\prim ->
           let isZeroArityEffect =
                   Logic.and (Equality.equal (Arity.primitiveArity prim) 0) (Logic.not (Packaging.primitiveDefinitionIsPure (Graph.primitiveDefinition prim)))
           in (Logic.ifElse isZeroArityEffect (tsCall varExpr []) varExpr)))
@@ -286,11 +286,11 @@
             mName = termHeadVariable headTerm
             argc = Lists.length args
             lazyMaybe =
-                    Optionals.cases mName Nothing (\n ->
+                    Optionals.match mName Nothing (\n ->
                       let lazyFlags = lazyFlagsForPrimitive g n
                           anyLazy = Lists.foldl (\b -> \f -> Logic.or b f) False lazyFlags
                       in (Logic.ifElse (Logic.and anyLazy (Equality.equal argc (Lists.length lazyFlags))) (Just (encodeLazyCall cx g currentNs headTerm args lazyFlags)) Nothing))
-        in (Optionals.cases lazyMaybe (
+        in (Optionals.match lazyMaybe (
           let dHead = Strip.deannotateAndDetypeTerm headTerm
               encArgs = Lists.map (encodeTerm cx g currentNs) args
           in case dHead of
@@ -323,7 +323,7 @@
       Core.TermPair v0 -> tsAsAny (tsArray [
         encodeTerm cx g currentNs (Pairs.first v0),
         (encodeTerm cx g currentNs (Pairs.second v0))])
-      Core.TermOptional v0 -> Optionals.cases v0 (tsAsAny (tsObject [
+      Core.TermOptional v0 -> Optionals.match v0 (tsAsAny (tsObject [
         ("tag", (tsExprStr "none"))])) (\v -> tsAsAny (tsObject [
         ("tag", (tsExprStr "given")),
         ("value", (encodeTerm cx g currentNs v))]))
@@ -386,7 +386,7 @@
                         Syntax.switchCaseConsequent = [
                           Syntax.StatementReturn (Just callExpr)]}) armFields
             defaultCase =
-                    Optionals.cases defaultMaybe (Syntax.SwitchCase {
+                    Optionals.match defaultMaybe (Syntax.SwitchCase {
                       Syntax.switchCaseTest = Nothing,
                       Syntax.switchCaseConsequent = [
                         Syntax.StatementReturn (Just (tsCall (tsExprIdent "(() => { throw new Error('unmatched case'); })") []))]}) (\dt ->
@@ -509,7 +509,7 @@
           Syntax.functionTypeExpressionReturnType = cod}))))
         Core.TypeVariable v0 ->
           let lname = Formatting.capitalize (Names.localNameOf v0)
-          in (Optionals.cases (Names.moduleNameOf v0) (Right (tsNamedType lname)) (\ns -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName ns)) (Right (tsNamedType lname)) (
+          in (Optionals.match (Names.moduleNameOf v0) (Right (tsNamedType lname)) (\ns -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName ns)) (Right (tsNamedType lname)) (
             let nsSegs = Lists.drop 1 (Strings.splitOn "." (Packaging.unModuleName ns))
                 typeAlias = Strings.concat2 "$type_" (Strings.join "_" nsSegs)
             in (Right (tsNamedType (Strings.concat [
@@ -596,7 +596,7 @@
 -- | Keep only names whose module differs from the current module's
 filterNonLocalNames :: Packaging.ModuleName -> S.Set Core.Name -> S.Set Core.Name
 filterNonLocalNames currentNs names =
-    Sets.fromList (Optionals.givens (Lists.map (\n -> Optionals.cases (Names.moduleNameOf n) Nothing (\nameNs -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName nameNs)) Nothing (Just n))) (Sets.toList names)))
+    Sets.fromList (Optionals.givens (Lists.map (\n -> Optionals.match (Names.moduleNameOf n) Nothing (\nameNs -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName nameNs)) Nothing (Just n))) (Sets.toList names)))
 
 -- | Walk an application spine, returning the innermost head term and its arguments in application order
 flattenApplication :: Core.Term -> (Core.Term, [Core.Term])
@@ -671,7 +671,7 @@
 importsToText overlaySubs kind currentNs names =
 
       let pairs =
-              Optionals.givens (Lists.map (\n -> Optionals.cases (Names.moduleNameOf n) Nothing (\ns -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName ns)) Nothing (Just (ns, n)))) (Sets.toList names))
+              Optionals.givens (Lists.map (\n -> Optionals.match (Names.moduleNameOf n) Nothing (\ns -> Logic.ifElse (Equality.equal (Packaging.unModuleName currentNs) (Packaging.unModuleName ns)) Nothing (Just (ns, n)))) (Sets.toList names))
           transformLocal =
                   \s -> Logic.ifElse (Equality.equal kind "type") (Formatting.capitalize s) (Formatting.sanitizeWithUnderscores Language.typeScriptReservedWords s)
           importKeyword = Logic.ifElse (Equality.equal kind "type") "import type" "import"
@@ -718,7 +718,7 @@
 -- | Look up a primitive by name and return its per-parameter laziness flags in parameter order
 lazyFlagsForPrimitive :: Graph.Graph -> Core.Name -> [Bool]
 lazyFlagsForPrimitive g name =
-    Optionals.cases (Maps.lookup name (Graph.graphPrimitives g)) [] (\prim -> Lists.map (\p -> Typing.parameterIsLazy p) (Typing.termSignatureParameters (Packaging.primitiveDefinitionSignature (Graph.primitiveDefinition prim))))
+    Optionals.match (Maps.lookup name (Graph.graphPrimitives g)) [] (\prim -> Lists.map (\p -> Typing.parameterIsLazy p) (Typing.termSignatureParameters (Packaging.primitiveDefinitionSignature (Graph.primitiveDefinition prim))))
 
 -- | True when a let-binding's value should be thunked to preserve Haskell's lazy evaluation semantics
 letBindingIsThunkCandidate :: Core.Binding -> Bool
@@ -731,7 +731,7 @@
                     _ -> False
           freeVars = Variables.freeVariablesInTerm (Core.bindingTerm b)
           callsShow =
-                  Lists.foldl (\acc -> \n -> Logic.or acc (Optionals.cases (Names.moduleNameOf n) False (\mn -> Equality.equal (Lists.take 2 (Strings.splitOn "." (Packaging.unModuleName mn))) [
+                  Lists.foldl (\acc -> \n -> Logic.or acc (Optionals.match (Names.moduleNameOf n) False (\mn -> Equality.equal (Lists.take 2 (Strings.splitOn "." (Packaging.unModuleName mn))) [
                     "hydra",
                     "show"]))) False (Sets.toList freeVars)
       in (Logic.and (Logic.not isLambda) callsShow)
@@ -739,7 +739,7 @@
 -- | Build a documentation comment from an optional description string, or nothing if the description is missing or empty
 mkDocComment :: Maybe String -> Maybe Syntax.DocumentationComment
 mkDocComment mdesc =
-    Optionals.cases mdesc Nothing (\d -> Logic.ifElse (Equality.equal d "") Nothing (Just (Syntax.DocumentationComment {
+    Optionals.match mdesc Nothing (\d -> Logic.ifElse (Equality.equal d "") Nothing (Just (Syntax.DocumentationComment {
       Syntax.documentationCommentDescription = (PrintDocs.renderDocStringWith tsDocEntityRef d),
       Syntax.documentationCommentTags = []})))
 
@@ -755,7 +755,7 @@
           typeImportsFromTypes =
                   Lists.foldl (\acc -> \td -> Sets.union acc (collectImports currentNs (Core.typeSchemeBody (Packaging.typeDefinitionBody td)))) Sets.empty typeDefs
           typeImportsFromTerms =
-                  Lists.foldl (\acc -> \td -> Optionals.cases (Packaging.termDefinitionSignature td) acc (\sig -> Sets.union acc (collectImports currentNs (Core.typeSchemeBody (Scoping.termSignatureToTypeScheme sig))))) Sets.empty termDefs
+                  Lists.foldl (\acc -> \td -> Optionals.match (Packaging.termDefinitionSignature td) acc (\sig -> Sets.union acc (collectImports currentNs (Core.typeSchemeBody (Scoping.termSignatureToTypeScheme sig))))) Sets.empty termDefs
           typeImportsFromInner =
                   Lists.foldl (\acc -> \td -> Sets.union acc (collectInnerTypeImports currentNs (Packaging.termDefinitionBody td))) Sets.empty termDefs
           typeImports = Sets.union (Sets.union typeImportsFromTypes typeImportsFromTerms) typeImportsFromInner
@@ -768,14 +768,14 @@
         let termItems = Lists.map (encodeTermDefinition cx g currentNs) termDefs
             allItems = Lists.concat2 typeItems termItems
             mModuleDoc = Optionals.bind (Packaging.moduleMetadata mod) (\em -> Packaging.entityMetadataDescription em)
-            moduleDocText = Optionals.cases mModuleDoc "" (\d -> Strings.concat2 (Serde.toTypeScriptComments d []) "\n\n")
+            moduleDocText = Optionals.match mModuleDoc "" (\d -> Strings.concat2 (Serde.toTypeScriptComments d []) "\n\n")
             header = Strings.concat2 "// Note: this is an automatically generated file. Do not edit.\n\n" moduleDocText
             renderItem =
                     \docAndItem ->
                       let mdoc = Pairs.first docAndItem
                           item = Pairs.second docAndItem
                           itemText = printModuleItem item
-                      in (Optionals.cases mdoc itemText (\d -> Strings.concat [
+                      in (Optionals.match mdoc itemText (\d -> Strings.concat [
                         Serde.toTypeScriptComments d [],
                         "\n",
                         itemText]))
@@ -798,7 +798,7 @@
           extClause =
                   Logic.ifElse (Lists.null exts) "" (Strings.concat2 " extends " (Strings.join ", " (Lists.map printTypeExpression exts)))
           members = Syntax.interfaceDeclarationMembers decl
-          renderMember = \ps -> Strings.join "\n  " (Strings.lines (printPropertySignature ps))
+          renderMember = \ps -> Strings.join "\n  " (Formatting.lines (printPropertySignature ps))
           body =
                   Logic.ifElse (Lists.null members) "" (Strings.concat [
                     "\n  ",
@@ -846,7 +846,7 @@
                     (Logic.ifElse (Syntax.propertySignatureOptional ps) "?" ""),
                     ": ",
                     (printTypeExpression (Syntax.propertySignatureType ps))]
-      in (Optionals.cases mcomments line (\dc -> Strings.concat [
+      in (Optionals.match mcomments line (\dc -> Strings.concat [
         Serde.toTypeScriptComments (Syntax.documentationCommentDescription dc) (Syntax.documentationCommentTags dc),
         "\n",
         line]))
@@ -908,7 +908,7 @@
 
       let name = Syntax.unIdentifier (Syntax.typeParameterName tp)
           constraint = Syntax.typeParameterConstraint tp
-      in (Optionals.cases constraint name (\c -> Strings.concat [
+      in (Optionals.match constraint name (\c -> Strings.concat [
         name,
         " extends ",
         (printTypeExpression c)]))
diff --git a/src/main/haskell/Hydra/TypeScript/Language.hs b/src/main/haskell/Hydra/TypeScript/Language.hs
--- a/src/main/haskell/Hydra/TypeScript/Language.hs
+++ b/src/main/haskell/Hydra/TypeScript/Language.hs
@@ -36,7 +36,7 @@
 import qualified Hydra.Util as Util
 import qualified Hydra.Validation as Validation
 import qualified Hydra.Variants as Variants
-import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
+import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, lines, map, pure, sum, unlines)
 import qualified Data.Scientific as Sci
 import qualified Data.Set as S
 
diff --git a/src/main/haskell/Hydra/TypeScript/Operators.hs b/src/main/haskell/Hydra/TypeScript/Operators.hs
--- a/src/main/haskell/Hydra/TypeScript/Operators.hs
+++ b/src/main/haskell/Hydra/TypeScript/Operators.hs
@@ -34,7 +34,7 @@
 import qualified Hydra.Util as Util
 import qualified Hydra.Validation as Validation
 import qualified Hydra.Variants as Variants
-import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
+import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, lines, map, pure, sum, unlines)
 import qualified Data.Scientific as Sci
 
 -- | Addition operator (+)
diff --git a/src/main/haskell/Hydra/TypeScript/Serde.hs b/src/main/haskell/Hydra/TypeScript/Serde.hs
--- a/src/main/haskell/Hydra/TypeScript/Serde.hs
+++ b/src/main/haskell/Hydra/TypeScript/Serde.hs
@@ -16,6 +16,7 @@
 import qualified Hydra.Error.System as ErrorSystem
 import qualified Hydra.Errors as Errors
 import qualified Hydra.File as File
+import qualified Hydra.Formatting as Formatting
 import qualified Hydra.Graph as Graph
 import qualified Hydra.Json.Model as Model
 import qualified Hydra.Overlay.Haskell.Lib.Equality as Equality
@@ -46,7 +47,7 @@
 import qualified Hydra.Util as Util
 import qualified Hydra.Validation as Validation
 import qualified Hydra.Variants as Variants
-import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
+import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, lines, map, pure, sum, unlines)
 import qualified Data.Scientific as Sci
 
 -- | True iff every codepoint is an ASCII digit 0-9, and the list is non-empty
@@ -69,7 +70,7 @@
 -- | Convert an array pattern to an AST expression
 arrayPatternToExpr :: [Maybe Syntax.Pattern] -> Ast.Expr
 arrayPatternToExpr arr =
-    Serialization.bracketList Serialization.inlineStyle (Lists.map (\maybeP -> Optionals.cases maybeP (Serialization.cst "") patternToExpr) arr)
+    Serialization.bracketList Serialization.inlineStyle (Lists.map (\maybeP -> Optionals.match maybeP (Serialization.cst "") patternToExpr) arr)
 
 -- | Convert an arrow function expression to an AST expression
 arrowFunctionExpressionToExpr :: Syntax.ArrowFunctionExpression -> Ast.Expr
@@ -182,7 +183,7 @@
 -- | Convert a break statement to an AST expression
 breakStatementToExpr :: Maybe Syntax.Identifier -> Ast.Expr
 breakStatementToExpr b =
-    Optionals.cases b (Serialization.cst "break;") (\label -> Serialization.suffix ";" (Serialization.spaceSep [
+    Optionals.match b (Serialization.cst "break;") (\label -> Serialization.suffix ";" (Serialization.spaceSep [
       Serialization.cst "break",
       (identifierToExpr label)]))
 
@@ -218,7 +219,7 @@
       let param = Syntax.catchClauseParam c
           body = Syntax.catchClauseBody c
           catchKw =
-                  Optionals.cases param (Serialization.cst "catch") (\p -> Serialization.spaceSep [
+                  Optionals.match param (Serialization.cst "catch") (\p -> Serialization.spaceSep [
                     Serialization.cst "catch",
                     (Serialization.parens (patternToExpr p))])
       in (Serialization.spaceSep [
@@ -233,7 +234,7 @@
           superClass = Syntax.classDeclarationSuperClass cls
           body = Syntax.classDeclarationBody cls
           extendsClause =
-                  Optionals.cases superClass [] (\s -> [
+                  Optionals.match superClass [] (\s -> [
                     Serialization.cst "extends",
                     (expressionToExpr s)])
           bodyExpr =
@@ -252,7 +253,7 @@
 
       let body = Syntax.classDeclarationWithCommentsBody cdwc
           mc = Syntax.classDeclarationWithCommentsComments cdwc
-      in (Optionals.cases mc (classDeclarationToExpr body) (\c -> Serialization.newlineSep [
+      in (Optionals.match mc (classDeclarationToExpr body) (\c -> Serialization.newlineSep [
         documentationCommentToExpr c,
         (classDeclarationToExpr body)]))
 
@@ -283,7 +284,7 @@
 -- | Convert a continue statement to an AST expression
 continueStatementToExpr :: Maybe Syntax.Identifier -> Ast.Expr
 continueStatementToExpr c =
-    Optionals.cases c (Serialization.cst "continue;") (\label -> Serialization.suffix ";" (Serialization.spaceSep [
+    Optionals.match c (Serialization.cst "continue;") (\label -> Serialization.suffix ";" (Serialization.spaceSep [
       Serialization.cst "continue",
       (identifierToExpr label)]))
 
@@ -316,11 +317,11 @@
           mparamName = Syntax.documentationTagParamName tag
           description = Syntax.documentationTagDescription tag
           typePart =
-                  Optionals.cases mtype "" (\t -> Strings.concat [
+                  Optionals.match mtype "" (\t -> Strings.concat [
                     "{",
                     (typeExpressionToString t),
                     "}"])
-          paramPart = Optionals.cases mparamName "" (\p -> Syntax.unIdentifier p)
+          paramPart = Optionals.match mparamName "" (\p -> Syntax.unIdentifier p)
           parts =
                   [
                     Strings.concat2 "@" name,
@@ -348,7 +349,7 @@
       let exported = Syntax.exportAllDeclarationExported a
           source = Syntax.exportAllDeclarationSource a
           exportedClause =
-                  Optionals.cases exported (Serialization.cst "*") (\e -> Serialization.spaceSep [
+                  Optionals.match exported (Serialization.cst "*") (\e -> Serialization.spaceSep [
                     Serialization.cst "*",
                     (Serialization.cst "as"),
                     (identifierToExpr e)])
@@ -404,7 +405,7 @@
       Syntax.ExpressionNew v0 -> Serialization.spaceSep [
         Serialization.cst "new",
         (callExpressionToExpr v0)]
-      Syntax.ExpressionYield v0 -> Optionals.cases v0 (Serialization.cst "yield") (\e -> Serialization.spaceSep [
+      Syntax.ExpressionYield v0 -> Optionals.match v0 (Serialization.cst "yield") (\e -> Serialization.spaceSep [
         Serialization.cst "yield",
         (expressionToExpr e)])
       Syntax.ExpressionAwait v0 -> Serialization.spaceSep [
@@ -469,11 +470,11 @@
           update = Syntax.forStatementUpdate f
           body = Syntax.forStatementBody f
           initExpr =
-                  Optionals.cases init (Serialization.cst "") (\i -> case i of
+                  Optionals.match init (Serialization.cst "") (\i -> case i of
                     Syntax.ForInitVariable v0 -> variableDeclarationToExpr v0
                     Syntax.ForInitExpression v0 -> expressionToExpr v0)
-          testExpr = Optionals.cases test (Serialization.cst "") expressionToExpr
-          updateExpr = Optionals.cases update (Serialization.cst "") expressionToExpr
+          testExpr = Optionals.match test (Serialization.cst "") expressionToExpr
+          updateExpr = Optionals.match update (Serialization.cst "") expressionToExpr
       in (Serialization.spaceSep [
         Serialization.cst "for",
         (Serialization.parenListAdaptive [
@@ -515,7 +516,7 @@
 
       let body = Syntax.functionDeclarationWithCommentsBody fdwc
           mc = Syntax.functionDeclarationWithCommentsComments fdwc
-      in (Optionals.cases mc (functionDeclarationToExpr body) (\c -> Serialization.newlineSep [
+      in (Optionals.match mc (functionDeclarationToExpr body) (\c -> Serialization.newlineSep [
         documentationCommentToExpr c,
         (functionDeclarationToExpr body)]))
 
@@ -531,7 +532,7 @@
           asyncKw = Logic.ifElse async [
                 Serialization.cst "async"] []
           funcKw = Logic.ifElse generator (Serialization.cst "function*") (Serialization.cst "function")
-          nameExpr = Optionals.cases mid [] (\id -> [
+          nameExpr = Optionals.match mid [] (\id -> [
                 identifierToExpr id])
           paramsExpr = Serialization.parenListAdaptive (Lists.map patternToExpr params)
       in (Serialization.spaceSep (Lists.concat [
@@ -559,7 +560,7 @@
                     Serialization.cst "if",
                     (Serialization.parens (expressionToExpr test)),
                     (statementToExpr consequent)]
-      in (Optionals.cases alternate ifPart (\alt -> Serialization.spaceSep [
+      in (Optionals.match alternate ifPart (\alt -> Serialization.spaceSep [
         ifPart,
         (Serialization.cst "else"),
         (statementToExpr alt)]))
@@ -700,7 +701,7 @@
 
       let body = Syntax.moduleItemWithCommentsBody miwc
           mc = Syntax.moduleItemWithCommentsComments miwc
-      in (Optionals.cases mc (moduleItemToExpr body) (\c -> Serialization.newlineSep [
+      in (Optionals.match mc (moduleItemToExpr body) (\c -> Serialization.newlineSep [
         documentationCommentToExpr c,
         (moduleItemToExpr body)]))
 
@@ -712,7 +713,7 @@
           source = Syntax.namedExportSource n
           specExprs = Lists.map exportSpecifierToExpr specifiers
           fromClause =
-                  Optionals.cases source [] (\s -> [
+                  Optionals.match source [] (\s -> [
                     Serialization.cst "from",
                     (stringLiteralToExpr s)])
       in (Serialization.suffix ";" (Serialization.spaceSep (Lists.concat [
@@ -798,7 +799,7 @@
 -- | Convert a return statement to an AST expression
 returnStatementToExpr :: Maybe Syntax.Expression -> Ast.Expr
 returnStatementToExpr r =
-    Optionals.cases r (Serialization.cst "return;") (\e -> Serialization.suffix ";" (Serialization.spaceSep [
+    Optionals.match r (Serialization.cst "return;") (\e -> Serialization.suffix ";" (Serialization.spaceSep [
       Serialization.cst "return",
       (expressionToExpr e)]))
 
@@ -847,7 +848,7 @@
       let test = Syntax.switchCaseTest c
           consequent = Syntax.switchCaseConsequent c
           caseLabel =
-                  Optionals.cases test (Serialization.cst "default:") (\t -> Serialization.spaceSep [
+                  Optionals.match test (Serialization.cst "default:") (\t -> Serialization.spaceSep [
                     Serialization.cst "case",
                     (expressionToExpr t),
                     (Serialization.cst ":")])
@@ -858,11 +859,11 @@
 switchStatementToExpr switchStmt =
 
       let discriminant = Syntax.switchStatementDiscriminant switchStmt
-          cases = Syntax.switchStatementCases switchStmt
+          match = Syntax.switchStatementCases switchStmt
       in (Serialization.spaceSep [
         Serialization.cst "switch",
         (Serialization.parens (expressionToExpr discriminant)),
-        (Serialization.curlyBlock Serialization.fullBlockStyle (Serialization.newlineSep (Lists.map switchCaseToExpr cases)))])
+        (Serialization.curlyBlock Serialization.fullBlockStyle (Serialization.newlineSep (Lists.map switchCaseToExpr match)))])
 
 -- | Convert a template literal to an AST expression
 templateLiteralToExpr :: Syntax.TemplateLiteral -> Ast.Expr
@@ -885,14 +886,14 @@
 -- | Convert a string to a TypeScript line comment. Empty source lines emit `//` (no trailing space).
 toLineComment :: String -> String
 toLineComment s =
-    Strings.join "\n" (Lists.map (\line -> Logic.ifElse (Equality.equal line "") "//" (Strings.concat2 "// " line)) (Strings.lines s))
+    Strings.join "\n" (Lists.map (\line -> Logic.ifElse (Equality.equal line "") "//" (Strings.concat2 "// " line)) (Formatting.lines s))
 
 -- | Format a description and tags as a JSDoc comment. Empty doc lines emit ` *` (no trailing space) so blank lines don't carry trailing whitespace.
 toTypeScriptComments :: String -> [Syntax.DocumentationTag] -> String
 toTypeScriptComments desc tags =
 
       let descLines =
-              Logic.ifElse (Equality.equal desc "") [] (Lists.map (\line -> Logic.ifElse (Equality.equal line "") " *" (Strings.concat2 " * " line)) (Strings.lines desc))
+              Logic.ifElse (Equality.equal desc "") [] (Lists.map (\line -> Logic.ifElse (Equality.equal line "") " *" (Strings.concat2 " * " line)) (Formatting.lines desc))
           tagLines = Lists.map documentationTagToLine tags
           allLines =
                   Lists.concat [
@@ -916,10 +917,10 @@
                   Serialization.spaceSep [
                     Serialization.cst "try",
                     (blockStatementToExpr block)]
-          catchPart = Optionals.cases handler [] (\c -> [
+          catchPart = Optionals.match handler [] (\c -> [
                 catchClauseToExpr c])
           finallyPart =
-                  Optionals.cases finalizer [] (\f -> [
+                  Optionals.match finalizer [] (\f -> [
                     Serialization.spaceSep [
                       Serialization.cst "finally",
                       (blockStatementToExpr f)]])
@@ -1057,7 +1058,7 @@
 
       let id = Syntax.variableDeclaratorId decl
           init = Syntax.variableDeclaratorInit decl
-      in (Optionals.cases init (patternToExpr id) (\e -> Serialization.ifx Operators.defineOp (patternToExpr id) (expressionToExpr e)))
+      in (Optionals.match init (patternToExpr id) (\e -> Serialization.ifx Operators.defineOp (patternToExpr id) (expressionToExpr e)))
 
 -- | Convert a variable kind to an AST expression
 variableKindToExpr :: Syntax.VariableKind -> Ast.Expr
diff --git a/src/main/haskell/Hydra/TypeScript/Syntax.hs b/src/main/haskell/Hydra/TypeScript/Syntax.hs
--- a/src/main/haskell/Hydra/TypeScript/Syntax.hs
+++ b/src/main/haskell/Hydra/TypeScript/Syntax.hs
@@ -5,7 +5,7 @@
 module Hydra.TypeScript.Syntax where
 
 import qualified Hydra.Core as Core
-import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
+import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, lines, map, pure, sum, unlines)
 import qualified Data.Scientific as Sci
 import qualified Data.Int as I
 
