みなさんはパイプライン演算子が欲しいですか?私は欲しいです.みなさんも欲しいですよね.ええ,そうです.みなさんはパイプライン演算子を求めています.
ですが TC39 の proposal-pipeline-operatorは Stage 2 から一向に動く気配を見せません.
これでは困りましたね.敬虔な関数型言語信者のみなさんであれば,パイプライン演算子のない世界のコードを書いたり読んだりするのが辛いということを知っているはずです.
現に TypeScript ではパイプライン演算子が無いために,こんなことをしています.なんということでしょう.
// 内側から外側へ読むsum(map([1, 2, 3], (n) => n * 2));
// あるいは pipe 関数で頑張るpipe( [1, 2, 3], map((n) => n * 2), sum,);かたやお隣,とは言い難いですが Rust に目を向けると macro_rules!なるマクロ展開がサポートされているではありませんか.TypeScript にもこういう仕組みがあれば構文拡張もできそうですね.
…え,あるんですか?
あるみたいですよ.それが今回紹介する Sweetenerです.
Sweetener とは何か
Sweetener はマクロを定義・使用するための独自構文 .stsから .tsを生成するためのライブラリ・ビルドツールです.
とりあえずインストールしましょう.
npm install --save-dev @sweetener/clinpx sweetener init試しに,Ruby の unlessを定義してみます.ifと逆の動作をします.
export syntax unless:stmt { rule { unless ($condition:expr) { $($body:stmt)* } } => { if (!($condition)) { $($body)* } }}このようにして定義したマクロは以下のように使うことができます.
import { unless } from "./macros.sts" for syntax;
export function withdraw(balance: number, amount: number): string { unless (amount <= balance) { return "insufficient funds"; } return `withdrew ${String(amount)}`;}これは単純な TypeScript コードに展開され,以下のようなコードになります.
展開には以下のコマンドを使います.
npx sweetener expand src/main.stsexport function withdraw(balance: number, amount: number): string { if (!(amount <= balance)) { return "insufficient funds"; } return `withdrew ${String(amount)}`;}型検査はこの展開後のコードに対して tscが行うため,Sweetener が特有の型チェックを行うことはありません.
また,マクロの importには for syntaxをつける必要があり,これは展開後の TypeScript コードには出力されません.
マクロを読む
unlessマクロを読み解いてみます.
export syntax unless:stmt { rule { unless ($condition:expr) { $($body:stmt)* } } => { if (!($condition)) { $($body)* } }}syntax unless:stmtで,unlessという名前のマクロを「文(stmt) が書ける位置」に定義しています.rule { ... }の中が実際に展開されるパターンです.()内で1個の式を$conditionとして,{}内で0個以上の文を$bodyとしてキャプチャしています.$(...)*は繰り返し
=>以降は,実際に TypeScript として展開されるコードを書きます.キャプチャした条件式を反転し,その後のブロックに文を渡すことでunlessの動作を実装しています.
パイプラインマクロを実装してみる
次に Elixir スタイル1のパイプライン演算子を実装するマクロを書いてみます.
export operator (|>):expr { fixity infix; associativity left; precedence 35;
rule { $value:expr |> $function:ident $(. $member:ident)* ($($argument:expr),*) } => { $function $(. $member)*($value #if(present $argument) {, $($argument), *}) }
rule { $value:expr |> $function:ident $(. $member:ident)* } => { $function $(. $member)*($value) }}import { (|>) } from "./operators.sts" for syntax;
const result = [1, 2, 3] |> map((n) => n * 2) |> sum;このコードは TypeScript に展開されたとき,このようになります.
const result = sum(map([1, 2, 3], (n) => n * 2));なるほど,確かにパイプライン演算子として機能するようにマクロが展開されていますね.
TC39 で提案されている Hack スタイル(x |> f(%)のように %で代入される値の位置を指定する) のパイプライン演算子も書くことができます.
他の活用例
すでに公式の例としては
- Effect-TS のボイラープレートをマクロとして展開する
effect f(x: string) { ... }をconst f = (x: string) => Effect.gen(function* () { ... })にするconst x = yield* f()をx <- f()にするerror NotFound { id: string }をData.TaggedErrorにするservice DatabaseをContext.Tagにするhandle f() { errors }をEffect.catchTagにする
- Drizzle のテーブル定義 DSL
などがあり,Effect-TS, Drizzle ユーザー2としてはかなり魅力的に見えました.
Effect-TS は特にボイラープレートが多く,読むのが結構つらいので,これらのマクロでコードレビュー時の読みづらさが軽減されるのではないかと期待していたり.
現状の制約
なんといってもまだ alpha 版という点です.以下の点においてはまだ改善の余地がありそうです.
エディタの支援がハイライトだけ
LSP やそれに付随するエディタ支援機能の実装自体は PR として上がっているため,それがマージされるとかなり開発しやすくなりそうです.
型ごとのマクロ展開を書けない
展開は型検査より前に行われるので,型内部によってマクロの展開方法を変えるなどの記述は現時点では行えません.
.sts, .stsx のフォーマットに Biome が対応していない
現状 .sts, .stsxは Prettier でしかフォーマットできません.普段私が使っているのは Biome なので,ここはつらいなあと思いました.
おわりに
ガッツリ実戦投入できるパターンはそこまで多くないとは思いますが,ボイラープレートを排除するための手段として知っておくと良さそうだなーという感じです.
マクロ・メタプログラミングに代表される構文の hack はどこまでやっていいのかチームや状況によって大きく変わると思います.個人開発ならこれですべてを破壊するのも悪くないですね
邪悪なマクロを組まない限り,構文としてはシンプルになるでしょうから,少なくとも人間の認知負荷に対しては良い効果をもたらしてくれると期待しています.
AI エージェントに対してはトークン効率などの面でベンチマークを取る必要がありそうですが.実際どこまでついてきてくれるかは気になるところです.
TypeScript で今まで出来なかったことや,記述量が多くなってしまっていた部分を解決するので,他の言語から TypeScript, Effect-TS などに移ってきたい場合に使える…のか?わからん.誰か実証してください.
この記事で上げたもの以外にも便利なマクロの使い道を思いついたら Zenn の Scrap, 便利な Sweetener マクロ n 選に随時追記していくので,気になったら見てみてください.
Footnotes
左辺の評価結果を,右辺の関数の第一引数に暗黙的に渡すスタイル. ↩
こんなツイートをしていたりします
↩I ❤ @EffectTS_ & @DrizzleORM !#Kirisame3D pic.twitter.com/ZIzYb0MX89
— ありあな (@Alliana_VRC) August 8, 2026