A TypeScript preprocessor that adds custom syntax for the typesugar macro system. Written in Rust on top of SWC.
sugarcube extends TypeScript with three syntax forms, then compiles them away into standard TypeScript that tsc and typesugar can process.
Input — sugarcube syntax:
interface Functor<F<_>> {
map: <A, B>(fa: F<A>, f: (a: A) => B) => F<B>;
}
const result = data |> parse |> validate;
const list = 1 :: 2 :: 3 :: [];Output — standard TypeScript (after sc preprocess):
interface Functor<F> {
map: <A, B>(fa: $<F, A>, f: (a: A) => B) => $<F, B>;
}
const result = __binop__(__binop__(data, "|>", parse), "|>", validate);
const list = __binop__(1, "::", __binop__(2, "::", __binop__(3, "::", [])));flowchart TD
Source["source.ts\n(sugarcube syntax)"] --> SC["sc preprocess"]
SC --> Valid["output.ts\n(standard TypeScript)"]
Valid --> Transformer["typesugar transformer\n(macro expansion)"]
Transformer --> JS["output.js"]
Valid --> TSC["tsc\n(type checking)"]
git clone https://github.com/typesugar/sugarcube.git
cd sugarcube
cargo install --path crates/sc_cli# Write some sugarcube-flavoured TypeScript
cat > example.ts << 'EOF'
const result = [1, 2, 3] |> map(x => x * 2) |> filter(x => x > 2);
const list = "a" :: "b" :: "c" :: [];
EOF
# Preprocess to standard TypeScript
sc preprocess example.ts
# Check syntax without emitting output
sc check example.ts
# Dump the AST as JSON (for debugging)
sc parse example.ts --ast| Command | Description | Flags |
|---|---|---|
sc preprocess <file> |
Parse, desugar, and emit standard TypeScript | -o <output>, --source-map, --tsx |
sc check <file> |
Parse and report syntax errors | --tsx |
sc parse <file> |
Parse and dump the AST | --ast (JSON output), --tsx |
All commands accept .ts and .tsx files. The --tsx flag enables JSX parsing explicitly; it's also inferred from the .tsx extension.
Chains values through functions left-to-right.
Grammar: expr |> expr — precedence 1 (lowest of sugarcube ops), left-associative.
Desugaring: a |> f → __binop__(a, "|>", f)
// Single step
const parsed = rawData |> parseJSON;
// → __binop__(rawData, "|>", parseJSON)
// Chained — left-to-right
const result = data |> parse |> validate |> transform;
// → __binop__(__binop__(__binop__(data, "|>", parse), "|>", validate), "|>", transform)
// Mixed with cons — :: binds tighter
const x = a :: b |> f;
// → __binop__(__binop__(a, "::", b), "|>", f)Edge cases:
|>inside strings and comments is not rewritten|>in type annotations / interfaces is not rewritten|followed by>with a space between is two separate tokens (bitwise OR, then greater-than), not a pipeline
Prepends an element to a list, ML-style.
Grammar: expr :: expr — precedence 5, right-associative.
Desugaring: a :: b → __binop__(a, "::", b)
// Single cons
const pair = 1 :: [];
// → __binop__(1, "::", [])
// Right-associative chaining — builds from the tail
const list = 1 :: 2 :: 3 :: [];
// → __binop__(1, "::", __binop__(2, "::", __binop__(3, "::", [])))
// With expressions
const xs = head :: tail;
// → __binop__(head, "::", tail)Edge cases:
::with a space between stays as two colons (e.g. type annotations likex: :foo)::inside type aliases and interfaces is not rewritten::binds tighter than|>— use parens to override
Declares type parameters as higher-kinded, then rewrites usages to the $ type operator.
Grammar:
- Declaration:
<F<_>>in a type parameter list (underscore placeholder) - Usage:
F<A>anywhere in the declaring scope →$<F, A>
Desugaring:
F<_>in declaration → stripped toFF<A>in scope →$<F, A>
// Declaration + usage
interface Functor<F<_>> {
map: <A, B>(fa: F<A>, f: (a: A) => B) => F<B>;
}
// → interface Functor<F> {
// → map: <A, B>(fa: $<F, A>, f: (a: A) => B) => $<F, B>;
// → }
// Multiple HKT params
type Transform<F<_>, G<_>> = <A>(fa: F<A>) => G<A>;
// → type Transform<F, G> = <A>(fa: $<F, A>) => $<G, A>;Edge cases:
F<A>outside the declaring scope is not rewritten- Only uppercase identifiers followed by
<_>are treated as HKT declarations - Nested scopes use the innermost matching declaration
Syntax extensions are controlled by ScSyntax feature flags. All are enabled by default:
ScSyntax {
pipeline: true, // |> operator
cons: true, // :: operator
hkt: true, // F<_> type parameters
}Disable individual extensions to avoid conflicts with other tooling or syntax you don't use.
sugarcube is the first stage of the typesugar compilation pipeline. The typical setup:
- sugarcube preprocesses
.tsfiles — rewrites|>,::,F<_>to standard TS - typesugar transformer (via ts-patch) expands macros like
__binop__and$<F, A> - tsc type-checks and emits JavaScript
When using unplugin-typesugar, sugarcube runs automatically before the transformer. See the typesugar docs for build tool integration.
crates/
sc_ast/ Extended AST types (ScBinExpr, HktTypeParam, ScSyntax)
sc_lexer/ Token merging (| + > → |>, : + : → ::)
sc_parser/ Text-level preprocessor + SWC parser wrapper
sc_desugar/ AST rewriting (pipeline, cons, HKT transforms)
sc_cli/ CLI binary (sc)
sc_test/ Test utilities
cargo build # all crates
cargo build -p sc_cli # just the CLI
cargo install --path crates/sc_cli # install locallycargo test # all tests
cargo test -p sc_parser # single crate
SC_UPDATE_FIXTURES=1 cargo test # update golden filescargo clippy --all-targets
cargo fmt --check