typescript-go

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

11posts · 30d
Nov 30 posts
  1. Zenn · · @onclimb

    JSX が JavaScript になるまで React のコンポーネントには、こんなコードを書きます。 return <h1 className="title">Hello, {name}!</h1>; 見た目はほとんど HTML です。でも、よく見ると HTML ではありません。React の JSX では、属性は class ではなく className ですし、{name} のように JavaScript の式がそのまま埋め込まれています。かといって、JavaScript にも return の後ろにタグを書く構文はありません。 では、ブラウザはこのタグを HTML として読んでいるのでしょうか。それとも JavaScript として読んでいるのでしょうか。 答えはどちらでもありません。ブラウザに届く時点で、このタグはもう残っていないからです。筆者も「ビルドのときに何かに変換されている」ことは知っていました。ただ、何に変換されて、ブラウザが最終的に何を受け取っているのかは説明できませんでした。この記事では、変換の途中でできるもの(中間生成物)を 1 つずつ取り出しながら、.jsx に書いたタグが JavaScript になり、実行されて何を返すのかまでを追いかけます。 この記事の前提 環境: React 19.3.1 / react-dom 19.3.1 / Node.js v26.12.1 / macOS ブラウザでの確認: Vite 8.3.5(@vitejs/plugin-react 6.1.2)/ Google Chrome 156.0.8078.80 変換ツール: Babel(@babel/core 8.0.9・@babel/preset-react 8.0.1・@babel/parser 8.0.8)を主に使い、比較用に TypeScript 7.0.2 の tsc / SWC(@swc/core 1.16.20)/ esbuild 0.28.2 / Oxc(oxc-transform 0.156.0)も使います。TSX のコラムでは、ほかに @babel/preset-typescript 8.0.7・@babel/plugin-syntax-typescript 8.0.3・@types/react 19.3.0 を使います 対象: React の標準の変換方式(automatic runtime)。断りがない限り、開発用の出力(development)はオフにした結果を載せています 題材: 小さなコンポーネントを書いた App.jsx が中心です。TypeScript(.tsx)の場合に何が変わるかは、末尾のコラムで扱います 範囲外: 変換後のオブジェクトが Fiber になり DOM に反映される過程(reconcile)、Hooks の実装、Server Components 記事中のログ・エラーメッセージ・出力は、すべて筆者が上記環境で実際に採取したものです。AST のダンプと 5 つのツールの比較は、それぞれのツールを Node.js から呼ぶ小さなスクリプトによるものです 全体像 パーサー — JSX を構文として読める。ソースを AST(構文木)にし、タグを JSXElement などのノードとして表す。各変換ツールが内蔵している(Babel なら @babel/parser) 変換 — AST の JSX のノードを関数呼び出し(_jsx(...))に書き換え、呼び出し先の import を差し込む。Babel・SWC・esbuild・Oxc・tsc などが担う ランタイム(react/jsx-runtime) — 変換後の _jsx(...) の呼び出し先。React Element と呼ばれるただのオブジェクトを返す レンダラー(react-dom) — React Element を受け取り、ブラウザでは DOM を組み立てる(サーバーでは HTML 文字列にもできる)。この記事では中身には踏み込まない ブラウザが受け取っているのは、タグが関数呼び出しに置き換わった普通の JavaScript です。 その JavaScript を実行したあと、react-dom が DOM を組み立てます。 どの関数をどの形で呼ぶかは、JSX の構文自体には決まっていません。React の automatic runtime では、呼ぶのは react/jsx-runtime の jsx(type, props, key) で、この形は React が定めた約束です。変換ツールは、呼び出し先の設定(importSource など)で指定された先に向けて、この約束どおりに書き換えます。そのため、どの変換ツールを使っても、ほぼ同じ JavaScript になります。 課題 — React を書いていて浮かぶ疑問 冒頭の疑問ともう 1 つ、次の 2 つに答えます。 ブラウザは JSX のタグを HTML として読んでいるのか、JavaScript として読んでいるのか JavaScript はスクリプト言語で、書いたファイルをそのまま渡せば動くはずなのに、なぜ React のプロジェクトはデプロイの前にビルドが要るのか 題材は、Fragment(<>)・属性・埋め込みの式・イベントハンドラ・key が一通り入った小さなコンポーネントです。 // App.jsx import { useState } from "react"; export function App({ name }) { const [count, setCount] = useState(0); return ( <> <h1 className="title">Hello, {name}!</h1> <button onClick={() => setCount(count + 1)}> count is {count} </button> {[1, 2].map((n) => <li key={n}>{n}</li>)} </> ); } 試したこと — 中間生成物を段階ごとに取り出す 段階 0: JavaScript のエンジンは JSX を読めない まず、変換をせずにそのまま実行してみます。JSX を含むファイルを .mjs として Node.js に渡しました。 $ node stage0/hello.mjs const el = <h1>hi</h1>; ^ SyntaxError: Unexpected token '<' タグの 1 文字目 < で構文エラーです。拡張子を .jsx にした場合は、プロジェクトの設定で結果が変わりました。package.json が "type": "module" なら構文を読む前に ERR_UNKNOWN_FILE_EXTENSION で弾かれ、CommonJS なら .js として読まれて同じ SyntaxError になります。 変換をしない静的なサーバーから、次の HTML を Chrome で開きました。 <script type="module"> const el = <h1>hi</h1>; </script> コンソールには、Node.js と同じエラーが出ました。 SyntaxError: Unexpected token '<' JSX は ECMAScript(JavaScript の標準仕様)には入っていない構文拡張です。JSX 自体の仕様の草案(react/jsx、旧 facebook/jsx)も、エンジンやブラウザが実装することは想定していないと冒頭で断っていて、何に変換されるかも定めていません。つまり、ブラウザは JSX を HTML としても JavaScript としても読めません。JSX は、ブラウザに届く前に必ず誰かが JavaScript に変換している構文です。1 つ目の疑問への答えの半分がこれです。 2 つ目の疑問「なぜビルドが要るのか」の答えの中心も、ここにあります。ブラウザに渡す前に JSX を JavaScript へ変換する工程が要り、それを担っているのがビルドです。開発中に同様の変換をしているのは開発サーバーで、その様子は段階 2 の最後で見ます。 段階 1: パーサー — タグは JSX 専用のノードになる 最初の仕事は、JSX を読めるパーサーで AST を作ることです。Babel のパーサー(@babel/parser に jsx プラグインを付けたもの)で、return (...) の中身をダンプしました。長いので <h1> と <button> の部分を抜きます。 JSXFragment JSXOpeningFragment JSXText "\n " JSXElement JSXOpeningElement JSXIdentifier "h1" JSXAttribute JSXIdentifier "className" StringLiteral "title" JSXText "Hello, " JSXExpressionContainer Identifier "name" JSXText "!" JSXClosingElement JSXIdentifier "h1" JSXText "\n " JSXElement JSXOpeningElement JSXIdentifier "button" JSXAttribute JSXIdentifier "onClick" JSXExpressionContainer ArrowFunctionExpression (略) JSXText "\n count is " JSXExpressionContainer Identifier "count" JSXText "\n " JSXClosingElement JSXIdentifier "button" (以下 map の JSXExpressionContainer) 普通の JavaScript の AST には無い、JSX で始まるノードが並んでいます。タグ名の h1 も、JavaScript の識別子を表す Identifier ではなく、JSXIdentifier という別の種類のノードです。大文字・小文字・ドット付きのタグを並べたファイルでも確かめました。 // Main.jsx import { App } from "./App.jsx"; export const page = <App name="World" />; export const tag = <app name="World" />; export const member = <ui.Button />; --- page JSXElement JSXOpeningElement JSXIdentifier "App" JSXAttribute JSXIdentifier "name" StringLiteral "World" --- tag JSXElement JSXOpeningElement JSXIdentifier "app" (略) --- member JSXElement JSXOpeningElement JSXMemberExpression JSXIdentifier "ui" JSXIdentifier "Button" Babel のパーサーでは、大文字の App も小文字の app も同じ JSXIdentifier でした。この時点ではまだ「HTML の要素か、自作のコンポーネントか」を区別していません(Oxc のように、パースの時点で区別するパーサーもあります)。 段階 2: 変換 — JSX が関数呼び出しになる App.jsx を Babel(@babel/preset-react、automatic runtime)で変換し、dist-jsx/App.js に書き出しました。 import { useState } from "react"; import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; export function App({ name }) { const [count, setCount] = useState(0); return /#PURE/_jsxs(_Fragment, { children: [/#PURE/_jsxs("h1", { className: "title", children: ["Hello, ", name, "!"] }), /#PURE/_jsxs("button", { onClick: () => setCount(count + 1), children: ["count is ", count] }), [1, 2].map(n => /#PURE/_jsx("li", { children: n }, n))] }); } タグが _jsxs("h1", { className: "title", children: [...] }) という関数呼び出しに書き換わりました。 2 行目の import は、ソースには書いていません。呼び出す関数(react/jsx-runtime の jsx / jsxs)がどこから来るかを書かないと動かないので、変換ツールが自分で差し込んだものです。_jsx は、その jsx を別名で import したものです。/#PURE/ は「戻り値を使っていなければ消してよい」というバンドラ向けの注釈(PURE 注釈)です。 出力には、ほかにも決まりがあります。 属性と子は 1 つのオブジェクト(props)にまとめられ、子は props.children に入る 改行を含む空白だけのテキストを除いて、ソースに書いた子が複数なら _jsxs、1 つ以下なら _jsx になる key は props から外れて第 3 引数になる(_jsx("li", { children: n }, n)。例外もありますが、ここでは扱いません) 大文字のタグは変数、小文字のタグは文字列 Babel のパーサーでは区別されていなかった大文字と小文字は、変換の段階で分かれます。Main.jsx の変換結果です。 import { App } from "./App.jsx"; import { jsx as _jsx } from "react/jsx-runtime"; export const page = /#PURE/_jsx(App, { name: "World" }); export const tag = /#PURE/_jsx("app", { name: "World" }); export const member = /#PURE/_jsx(ui.Button, {}); 大文字で始まる App は変数 App の参照に、小文字の app は文字列 "app" に、ドット付きの ui.Button はプロパティの参照になりました。コンポーネント名を大文字で始めなければいけないのは、このルールで HTML の要素名(文字列)と区別されるからです。 5 つのツールで変換して比べる 実際のプロジェクトで JSX を変換しているのは、Babel だけではありません。Next.js は既定で SWC を使い、Vite 8 では Vite 本体の Oxc が JSX を変換していて、@vitejs/plugin-react v6 は Babel への依存をなくしました。そこで、同じ App.jsx を Babel・SWC・esbuild・Oxc・tsc の 5 つで変換しました。設定は 3 つそろえています。automatic runtime、importSource は react、開発用の出力(development)はオフです。 生の出力は見た目がかなり違います。たとえば esbuild はこうです。 import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { useState } from "react"; function App({ name }) { const [count, setCount] = useState(0); return /* @PURE */ jsxs(Fragment, { children: [ /* @PURE */ jsxs("h1", { className: "title", children: [ "Hello, ", name, "!" ] }), /* @PURE */ jsxs("button", { onClick: () => setCount(count + 1), children: [ "count is ", count ] }), [1, 2].map((n) => /* @PURE */ jsx("li", { children: n }, n)) ] }); } export { App }; import の順番、_jsx か jsx かという名前、PURE 注釈の有無と書き方、整形、export の形がツールごとに違います。そこで import 文と PURE 注釈を消し、名前の先頭の _ と export の形をそろえ、整形し直してから比べるスクリプトを書きました。消した import 文も、import 元は 5 つとも react/jsx-runtime で、違うのは並び順と別名だけでした。 App: 1 distinct normalized output(s) [tsc, babel, swc, esbuild, oxc] function App({name}){const[count,setCount]=useState(0);return jsxs(Fragment,{children:[jsxs("h1",{className:"title",children:["Hello, ",name,"!"]}),jsxs("button",{onClick:()=>setCount(count+1),children:["count is ",count]}),[1,2].map(n=>jsx("li",{children:n},n))]})} 5 つの出力は、書き方の差を除くと完全に一致しました。Main.jsx でも同じで、5 つのツールすべてが大文字のタグを変数、小文字のタグを文字列に分けていました。ui はどこにも定義していませんが、どのツールもそのまま変換しています。変換ツールは、名前の正体を調べていません。 実装は 5 つともまったく別物で、言語も JavaScript(Babel)・Rust(SWC・Oxc)・Go(esbuild・tsc)とバラバラです。それでも出力がそろうのは、関数の形(jsx(type, props, key))と、そこへの渡し方(子は props.children に、key は第 3 引数に、子が複数なら jsxs)までが React の決めた約束で、どのツールもそれに合わせて書き換えているからです。変換ツールはその約束どおりに呼び出しを書けばよく、他のファイルを見る必要がありません。 なお、開発用の出力をオンにすると、同じ正規化でも 4 通りに分かれました。tsc・SWC・esbuild・Oxc はデバッグ用のソースの位置(ファイル名・行・列)を同じ値で渡しますが、ファイル名を変数に入れるか(tsc は const、Oxc は var)直接書くか(SWC・esbuild)が違い、Babel 8 は位置の情報そのものを渡しません。 ブラウザに届く JavaScript を確かめる この形のままブラウザに届くことを、実際のプロジェクトでも確かめました。同じ App.jsx を Vite 8 のプロジェクトに置き、vite build の出力から App の部分を抜き出しました。Vite 8 の変換は、先ほどの 5 つのうちの Oxc です。 return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(h1,{className:title,children:[Hello, ,e,!]}),(0,p.jsxs)(button,{onClick:()=>n(t+1),children:[count is ,t]}),[1,2].map(e=>(0,p.jsx)(li,{children:e},e))]}) minify(圧縮)で変数名が縮み、文字列の引用符もバッククォートになっています。p はバンドルに取り込まれた react/jsx-runtime を指す変数で、(0,p.jsxs)(...) は this を渡さずに p.jsxs を呼ぶための、バンドラの書き方です。形は Babel の出力と同じ jsxs("h1", { className: "title", children: [...] }) で、出力の中に <h1 という文字列は 1 つもありませんでした。 開発サーバー(vite)でも確かめました。Chrome が App.jsx を要求すると、Content-Type: text/javascript の JavaScript が返ってきます。ブラウザはモジュールとして読み込むファイルの種類を拡張子ではなく Content-Type で判断するので、名前が .jsx のままでも JavaScript として実行します。中身は開発用の _jsxDEV(...)(react/jsx-dev-runtime)の呼び出しでしたが、タグが残っていない点は同じでした。あわせて、import { useState } from "react" も、/node_modules/.vite/deps/react.js?v=… からの import と const useState = …["useState"] という形に書き換わっていました。react の npm パッケージは CommonJS で配布されていてブラウザではそのまま動かないので、Vite が事前に ESM へ変換しておいたファイルです。 ブラウザに届いているのは、タグが消えた関数呼び出しの JavaScript でした。これが 1 つ目の疑問の残り半分の答えです。 段階 3: ランタイム — jsx() はただのオブジェクトを返す では、jsx() は何を返すのでしょうか。変換後の App.js と同じ呼び方で react/jsx-runtime の jsx / jsxs を呼び、戻り値を出力しました(jsx は本来アプリから直接呼ぶ関数ではなく、ここでは観察のために呼んでいます。NODE_ENV=production での結果です)。 --- jsxs("h1", ...) { '$$typeof': Symbol(react.transitional.element), type: 'h1', key: null, ref: null, props: { className: 'title', children: [ 'Hello, ', 'World', '!' ] } } 返ってきたのは、ただのオブジェクトでした。これが React Element です。type にはタグ名の文字列 'h1'、props には属性と子がそのまま入っています。$$typeof の値 Symbol(react.transitional.element) は、このオブジェクトが React Element であることを示す印です。段階 2 で JSX は関数呼び出しになりましたが、その関数がやっているのは「この形のオブジェクトを作る」ことだけです。DOM の要素はまだどこにも作られていません。 コンポーネントでも同じです。App を、呼び出された回数を数える関数 Counted で包んで jsx() に渡すと、type に関数そのものが入ったオブジェクトが返ります。 --- jsx(Counted, { name }) { '$$typeof': Symbol(react.transitional.element), type: [Function: Counted], key: null, ref: null, props: { name: 'World' } } type is the function itself: true | component called: 0 この時点で Counted は 1 回も呼ばれていませんでした。<App /> と書いても、App() が実行されるわけではありません。「App という関数を、この props で呼んでほしい」と書いたオブジェクトができるだけです。いつ呼ぶか(呼ばないか)はレンダラーが決めます。 結果 最後に、Vite の開発サーバーで開いた Chrome のページで、react-dom が組み立てた DOM を見ました。ページ上で document.getElementById("root").innerHTML を評価し、#root の中身を文字列にして取り出したものです。 <h1 class="title">Hello, World!</h1><button>count is 0</button><li>1</li><li>2</li> ここで初めて、画面の要素(DOM)が出てきました。ブラウザでは HTML の文字列を作って流し込んでいるのではなく、react-dom が React Element をもとに DOM を直接組み立てています。className は DOM では class 属性になっています。className は DOM の要素が持つプロパティ名(element.className)に合わせた名前で、class 属性として反映するのはレンダラーの仕事です。 ここまでをまとめると、次のとおりです。 JSX は ECMAScript の構文ではなく、Node.js も Chrome もタグの 1 文字目で SyntaxError を出した(段階 0) Babel のパーサーは、タグを JSXElement・JSXText などのノードにした。大文字のタグも小文字のタグも同じ JSXIdentifier だった(段階 1) JSX は _jsx / _jsxs の呼び出しになり、呼び出し先(react/jsx-runtime)の import が自動で差し込まれた。大文字のタグは変数、小文字のタグは文字列になり、key は props から外れて第 3 引数になった(段階 2) 設定をそろえた 5 つの変換ツールの出力は、書き方の差を除いて一致した(段階 2) Vite のビルド出力にも開発サーバーの応答にも、タグは残っていなかった(段階 2) jsx() は $$typeof・type・key・ref・props を持つただのオブジェクトを返し、コンポーネント関数はこの時点で呼ばれていなかった(段階 3) DOM を組み立てるのは react-dom で、className は class 属性になった 学び JSX の変換は、設定が分かればファイル 1 枚で完結する。 呼び出し先(react/jsx-runtime)と呼び出しの形(jsx(type, props, key))がファイルの外で決まっているので、変換ツールは他のファイルを見ずに書き換えられる。だから開発サーバーは、ブラウザに要求されたファイルだけをその場で変換して返せる 変換後の JSX を実行すると、「何を描画してほしいか」を書いたただのオブジェクトができる。 コンポーネントを呼ぶのも DOM を作るのも、そのオブジェクトを受け取ったレンダラーの仕事 課題に挙げた 2 つの疑問に答えると、こうなります。 「ブラウザは JSX を HTML として読んでいるのか、JavaScript として読んでいるのか」 — タグのままでは、HTML としても JavaScript としても読めません(段階 0)。タグはブラウザに届く前に関数呼び出しへ置き換わっていて、ブラウザはそれを普通の JavaScript として実行します。画面の要素(DOM)は、その実行の結果としてレンダラーが組み立てたものです。 「なぜスクリプト言語の JavaScript なのにビルドが要るのか」 — 書いたファイルが、ブラウザがそのまま実行できる形になっていないからです。その代表が JSX で、JavaScript ではありません。開発サーバーは JSX だけでなく、from "react" のような名前だけの import も、CommonJS で配布されている react を ESM に変換したファイルの URL に書き換えていました。開発中はこの書き換えを開発サーバーがその場でしてくれますが、静的ファイルを配るだけのデプロイ先には、それをしてくれる仕組みがありません。だから、デプロイ前のビルドで済ませておきます。 JSX の挙動で迷ったときは、DevTools の Network タブで届いた App.jsx を開くなどして、変換後の JavaScript を一度のぞいてみるところから始めていきたいですね。 コラム: TSX の場合 — tsx → jsx → js なのか、tsx → ts → js なのか ここまでは .jsx で見てきましたが、TypeScript と組み合わせて .tsx で書くプロジェクトも多くあります。.tsx には型と JSX の 2 つが入っているので、JavaScript になるまでに「型を消す」と「JSX を変換する」の 2 つの変換が要ります。題材の App.tsx は、App.jsx に型を 2 か所足しただけのものです。 type Props = { name: string }; export function App({ name }: Props) { // (以下 App.jsx と同じ) 結論から言うと、どちらの順でもできて、結果は同じでした。automatic runtime では、2 つの変換は互いに独立しているからです(classic runtime では、型を消すときに使われていない import を消す処理が import React を消してしまうことがあり、完全には独立していません)。 まず tsx → jsx → js です。tsc の jsx: preserve は、App.tsx から型を消し、JSX は残したまま App.jsx を出します。 // tsc --jsx preserve の出力(App.jsx) export function App({ name }) { const [count, setCount] = useState(0); return (<> <h1 className="title">Hello, {name}!</h1> (略) これを Babel で変換すれば JavaScript になります。 もう一方の順番の tsx → ts → js も作れます。Babel に TypeScript の構文プラグイン(@babel/plugin-syntax-typescript。構文を読むだけで、型は消さない)を付けて JSX の変換をかけると、型が残ったまま JSX が関数呼び出しになりました。 // Babel(preset-react + TypeScript の構文プラグイン)の出力。型はまだ残っている type Props = { name: string; }; export function App({ name }: Props) { const [count, setCount] = useState(0); return /#PURE/_jsxs(_Fragment, { (略) これを esbuild で型を消して JavaScript にしました。2 つの経路の最終出力を、本文と同じスクリプトで直接変換の結果と比べると、すべて一致しました(出力の見出しは読みやすく書き換えています)。 App: 1 distinct normalized output(s) [直接(tsc), 直接(babel), tsx→ts→js, tsx→jsx→js, 直接(oxc)] TypeScript 7 の実装(typescript-go の typescript/v7.0.2 タグの internal/compiler/emitter.go)で、tsc の変換器(tsc の中で 1 つの変換を受け持つ部品)を並べている getScriptTransformers を見ると、型を消す変換器が先で、JSX の変換器が後でした。 // transform TypeScript syntax { // (中略) // erase types tx = append(tx, tstransforms.NewTypeEraserTransformer(&opts)) // (中略) } if jsxTransformEnabled { tx = append(tx, jsxtransforms.NewJSXTransformer(&opts)) } つまり tsc は tsx → jsx → js の順で、jsx: preserve は、この並びから JSX の変換器だけを外した姿です。ただし中間のファイルはできず、どの変換器も前の変換器が返した AST を受け取り、次の AST を返していくだけです。他のツールも中間のファイルは作りませんが、作りはツールごとに違います。たとえば esbuild は型注釈をパースの時点で読み飛ばすので、そもそも型が AST に残りません。 .tsx で差が出るのは、変換の順番よりも、パーサーの読み方と型検査のほうです。tsc は型を検査するために、import を辿ってコンパイル対象のファイル一式(Program)を集め、名前の正体を解決し、型を計算します。 .tsx という拡張子は、TypeScript のパーサーの読み方を切り替える。.ts では型アサーションとして通る <string>v が、.tsx では TS17008: JSX element 'string' has no corresponding closing tag になった。.tsx でジェネリックなアロー関数を <T,>(x: T) => x とカンマ付きで書くのはこのため tsc --traceResolution で見ると、ソースのどこにも書いていない react/jsx-runtime が、react より先に解決されていた。JSX の型を引くため tsc は @types/react の JSX 名前空間で JSX を検査する。h1 は JSX.IntrinsicElements という interface のプロパティとして解決され、className は HTMLAttributes のプロパティだった。class と書くと TS2322 … Property 'class' does not exist … Did you mean 'className'? になる(それでも JavaScript は出力される) 所要時間は全体で約 0.18 秒で、そのうち Check(型検査の工程)が約 0.16 秒、JSX の変換を含む書き出し(Emit)は 0.001 秒だった。--noCheck を付けると全体で 0.020 秒になる。React を含まない 1 行の .ts と比べると、React の型定義(@types/react の 3 ファイルと csstype)が Program に加わった分、計算された型の数が約 2 倍になっていた。この題材は skipLibCheck を付けていないので、増えた分のほとんどは JSX ではなく型定義ファイル自体の検査だ 変換はファイル 1 枚で終わるのに、検査は呼び出し先の型定義まで Program を広げないとできません。.tsx の JSX は、この 2 つの仕事の重さの差がはっきり見える場所でした。 コラム: React 以外の JSX JSX は Preact・Hono・Vue・Solid でも書けます。構文と AST(段階 0・1)はどのフレームワークでも共通で、違いが出るのは「何の関数を呼ぶか」(段階 2)と「その関数が何を返すか」(段階 3)です。 ! 以下は各フレームワークのドキュメントに基づく整理で、この記事では検証していません。 フレームワーク 変換のしかた(呼び出し先) 返すもの React react/jsx-runtime React Element(ただのオブジェクト) Preact preact/jsx-runtime VNode Hono hono/jsx/jsx-runtime HTML 文字列にできるノード Vue 3 専用の Babel プラグインが生成する関数呼び出し VNode Solid 専用のコンパイラが生成する DOM 操作のコード 実際の DOM ノード(ブラウザ向けの出力) React・Preact・Hono は、jsx-runtime という同じ形の約束の上で返すものだけを差し替えているので、変換ツールの呼び出し先の設定(importSource)を変えるだけで切り替わります。Vue にも vue/jsx-runtime はありますが、公式が案内しているのは専用の Babel プラグインです。Solid はこの約束を使わず、JSX を、DOM を直接組み立てるコードにコンパイルします。 参考 JSX の仕様草案: react.github.io React「Writing Markup with JSX」(className の由来): react.dev React ブログ「Introducing the New JSX Transform」(automatic runtime): legacy.reactjs.org Next.js Compiler(SWC): nextjs.org @vitejs/plugin-react の CHANGELOG(v6 で Babel への依存を削除): github.com/vitejs/vite-plugin-react/blob/main/pac… Oxc: oxc-project/oxc esbuild の設計(型注釈の読み飛ばし): github.com/evanw/esbuild/blob/main/docs/architect… typescript-go internal/compiler/emitter.go(v7.0.2): github.com/microsoft/typescript-go/blob/typescrip… Preact: preactjs.com Hono の JSX: hono.dev Vue「Render Functions & JSX」: vuejs.org Solid の JSX コンパイラ(dom-expressions): ryansolid/dom-expressions

  2. DEV · · @vladut02

    Why TypeScript 7 Is Written in Go, Not C#: 43 Years of Anders Hejlsberg --- title: "Why TypeScript 7 Is Written in Go, Not C#: 43 Years of Anders Hejlsberg" published: true description: From Turbo Pascal to the TypeScript 7 native port, one idea keeps coming back in Anders Hejlsberg's languages: the time between typing code and seeing what it does. tags: typescript, csharp, history, go cover_image: canonical_url: series: Vlad's Stack --- On July 8, 2026, TypeScript 7 shipped: the same compiler, ported to a new language, and about ten times faster. On the VS Code codebase, a full build went from 125.7 seconds to 10.6 seconds. The port had been announced in March 2025 by Anders Hejlsberg, the man who designed C#. And the new language wasn't C#. It was Go. To see why that makes sense, it helps to go back 43 years. Across four languages, one idea keeps coming back: the time between typing your code and seeing what it does. {% youtube youtube.com %} A compiler from Denmark In the early 1980s, Hejlsberg wrote a Pascal compiler for small personal computers in Denmark. It was sold as Compass Pascal, and later as Poly Pascal. A young California company, Borland, licensed it, and Hejlsberg stayed in Denmark: according to Dr. Dobb's, Borland sent him 9600-baud modems so he could send each new build to California. Turbo Pascal, 1983 On November 20, 1983, it shipped as Turbo Pascal. The numbers, again from Dr. Dobb's: - $49.95, when its nearest competitor cost nearly $500 - one floppy disk, ten files - TURBO.COM, the main program, was 33,280 bytes The editor and the compiler lived in one program. You typed, compiled and ran without leaving it. Hejlsberg later wrote that back in the old Turbo Pascal days, it was possible for one person to write and maintain an entire product. Delphi, 1995 Windows brought windows, buttons and menus, and building them by hand meant pages of code. On February 14, 1995, Borland launched Delphi in San Francisco, with Hejlsberg as chief architect. You drew a window, dropped a button on it and wrote what should happen on click. Underneath, it was still Pascal, grown out of Turbo Pascal. C# In 1996 he joined Microsoft. In June 2000, Microsoft announced C#, with Hejlsberg as lead designer (the ECMA-334 standard names him first among its principal inventors). The goal, as described at the time: the power of C++ with the ease of Visual Basic. That Delphi button followed him. In C#, a click calls your method through a delegate, and every event you write works that way. (How delegates ended up in C# is its own story, involving Sun and a court order: [Why C# Exists](VIDEO_CHANNEL_URL).) He stayed with C# as it grew: generics in 2005, LINQ in 2007, async/await in 2012. TypeScript, 2012 By 2012, people were building JavaScript applications of a size the language was never planned for. Some teams inside Microsoft used tools like Script# to write C# and compile it to JavaScript. TypeScript went the other way: don't hide JavaScript, put types on top of it. Microsoft introduced TypeScript on October 1, 2012. Every JavaScript program was already a TypeScript program, and the types are erased when you compile. So what are they for? For the tools: errors as you type, autocomplete, safe renames. It's the Turbo Pascal idea again. The fastest feedback is the one you get before you run the code. The slow compiler TypeScript took over. By 2025, VS Code alone was about 1.5 million lines of it. But the compiler was itself written in TypeScript, running as JavaScript. On VS Code, tsc took 77.8 seconds, and just opening the project in the editor took about 9.6 seconds. In March 2025, Hejlsberg wrote the announcement: a native port of the compiler. Not a rewrite. A port: the same logic, moved over piece by piece, so it behaves the same. One thing worth stating plainly: your TypeScript code didn't change. Only the program that checks it did. Why Go, not C# Plenty of C# developers asked. The team answered in public, in a “Why Go?” FAQ: - It's a port, so the code shape matters most. "Idiomatic Go strongly resembles the existing coding patterns of the TypeScript codebase", which is mostly plain functions and data structures rather than classes. - Memory control with a garbage collector. Go lets them control how data is laid out in memory, without managing memory by hand. - Batch builds can skip GC. "Batch compilations can effectively forego garbage collection entirely, since the process terminates at the end." In an interview with Michigan TypeScript in March 2025, as reported by devclass, Hejlsberg said they did consider C#, but that C# is bytecode-first and its native (AOT) compilation wasn't available on every platform they needed. So it wasn't a verdict on C#. It was the tool that fit this one job. TypeScript 7 TypeScript 7 shipped on July 8, 2026, with full builds typically 8 to 12 times faster. On VS Code, a full build went from 125.7 s to 10.6 s, and the time until the first error appears in the editor dropped from about 17.5 seconds to under 1.3. One idea, four languages Turbo Pascal, Delphi, C#, TypeScript. Different companies, different platforms. Each time, the tool around the language mattered as much as the language. And when his own compiler got slow, his team didn't reach for his own language. They reached for the one that fit the job. Would you have ported it to C#? Tell me in the comments. --- *I make Vlad's Stack — short explainers and stories about how the tools you use every day actually work, for people who write code (.NET-flavored). The video version of this story: youtube.com

  3. Zenn · · @yt4

    Go製TypeScript 7の公開から約3か月後、なぜRust製の別実装が相次いだのか はじめに 2026 年 7 月 8 日、Go で実装された TypeScript 7.0 が正式にリリースされました。公式発表では従来の TypeScript 6 と比べてフルビルドが概ね 8〜12 倍速くなったと報告されています。 そこから約 3 か月後の 10 月、Go 版をもとにした Rust 製の別実装の動きが目立つようになりました1。10 月 10 日には Bun 1.4.3 が bun check を公開し、20 日には Cloudflare と VoidZero が tsrs への協力体制 を発表しています。 tsc-rs も含め、Rust に移植するプロジェクトが複数進んでいます。公式の Go 実装も引き続き開発されており、Rust 版は別プロジェクトです。 TypeScript チームは Rust も検討したうえで Go を選んでいます。それなのに、今度は Rust への移植がいくつも進んでいるのが気になったため、これまでの経緯と各実装の特徴を調べてみました。 この記事では TypeScript 5 から Go 移植までを振り返り、各実装の特徴を見ながら、Rust 製の別実装が相次いだ背景を考えてみます。公開ベンチマークや、Rust 以外の方法で高速化する処理系も紹介します。 ※情報は 2026 年 10 月 27 日時点のものです。ベンチマークは開発元や測定者が公開した結果を引用しており、自分の環境で測定したものではありません。 型チェックとトランスパイルについて TypeScript の型注釈の除去や構文の変換によって JavaScript を生成するのがトランスパイルです。一方、代入や関数呼び出しが型のルールに合っているかを調べるのが型チェックです。 const count: number = "3"; このコードは型注釈を取り除けば JavaScript として実行できますが、型チェックではエラーになります。 esbuild は TypeScript を変換できますが、型チェックは行いません。公式ドキュメント でも、型チェックには別途 tsc --noEmit を使うよう説明されています。SWC や Oxc も TypeScript の変換機能を提供していますが、今回の中心は型チェック自体を高速化する処理系です。 TypeScript 5 から Go 移植まで TypeScript は 2012 年 10 月に公開 され、長く TypeScript で書かれたコンパイラを JavaScript にして動かしてきました。Go に移植する前にも、既存実装の高速化は続いています。 日付 出来事 コンパイラの変化 2025 年 3 月 11 日 Go への移植を発表 ネイティブコードと並列処理による高速化を目指す 2025 年 5 月 22 日 Native Preview 公開 npm から Go 版を試せるようになる 2026 年 3 月 23 日 TypeScript 6.0 公開 JavaScript ベースの最後のメジャーリリースとする方針 2026 年 7 月 8 日 TypeScript 7.0 公開 Go 製コンパイラを正式に提供 TypeScript 5.0 では、内部オブジェクトの構造をそろえて JavaScript エンジンが処理しやすくするなどの改善が行われました。7.0 ではネイティブコードで動かし、複数の CPU コアも利用するようになった、という流れです。 ちなみに、tsc は公式コンパイラのコマンド名で、7.0 では Go 製の実行ファイルを起動します。tsgo はプレビュー時のコマンド名であり、Go 実装を指す名前としても使われます。この記事でも、従来の JavaScript 実装と区別するときに使っています。 なぜ Go が選ばれたのか 既存の挙動を保って移植したかった 公式の Why Go? では、言語選定の最大の条件として既存実装との互換性を挙げています。型チェックの結果に加えて、コードの構造を似せることも重視していました。 旧実装と新実装を並行して保守するには、片方の変更をもう片方に移しやすい方が便利です。Go は既存のコーディングパターンに近く、メモリ配置を制御しながら、不要なメモリを回収する GC も使えます。 TypeScript の内部では、構文を表す AST、名前と宣言を結ぶ Symbol、型情報などが相互に参照します。構文の親子関係は木構造ですが、実装には子から親へ戻る参照もあり、参照をたどると循環します。こうした参照関係を保ちやすいことが、Go を選ぶ理由になっていました。 Rust への移植で難しかったこと Anders Hejlsberg は インタビュー で、Rust も試したものの、循環するデータ構造を多用する既存実装の移植には適さなかったと説明しています。 自分は GC を前提にした参照や後からの書き換えを、従来の構造を保って Rust に移す作業の難しさを説明している、と理解しました。Rust で循環参照を扱う方法は、この後の tsrs でも使われています。 Rust で書くなら、参照の表現やメモリを解放する単位などを別途設計できます。ただし、その分だけ移植の作業も増えます。互換性を保ちつつ少ない変更で高速化したい、という目的では Go が適していたということですね。 このあたりの歴史や構造は、TSKaigi 2026 の 制約と時代から読み解く TypeScript コンパイラ設計史 でも詳しく解説されています。 その後の TypeScript 処理系 2026 年 10 月時点の主な処理系を、ざっくり整理してみました。 処理系 実装と由来 主な特徴 tsc / tsgo Go 製の公式実装 互換性を比較する基準 tsrs Go 版を Rust に移植 メモリ配置の最適化、WebAssembly 対応 tsc-rs Go 版を Rust に移植した別プロジェクト AI による移植、Effect 向けの診断機能 bun check Go 版を移植し、Rust 製の Bun に統合 実行・テスト・ビルド前に型チェック tsz Rust 製の TypeScript 互換実装 Checker と Solver を分ける設計 tsc-ts Go 版を TypeScript に移植 scriptc でネイティブコードにコンパイル 移植元を図にすると、以下のようになります。tsz は設計を組み直した互換実装なので、同じ方式の移植としては結んでいません。 tsrs 2026 年 10 月 20 日、Cloudflare と VoidZero は、Owner.com の Max Schwenk 氏とともに tsrs に取り組むと発表しました。Oxc のツールチェーンの一部として開発し、TypeScript チームとも協力する方針です。 移植ガイド によると、tsrs は Go 版のアルゴリズムや関数の分割、処理順序を保つ方針です。一方、メモリ管理には独自の実装を使っています。 循環参照するオブジェクトは、メモリをまとめて管理するアリーナに確保します。オブジェクト同士は P<T> という型で参照します。 型チェック用のスレッド内だけで使うデータの更新には、Cell や RefCell などの内部可変性を使います。複数のスレッドで共有する AST などには独自の OwnedCell / FrozenCell を使い、原則として型チェック前に更新を終えます。遅延初期化が必要な共有データは、ロックなどで同期します。 こうするとオブジェクト間の参照関係と、メモリを所有・解放する単位を分けられます。親子が互いを参照していても、個々のノードが相手のメモリを所有する必要はありません。 Unix 向けの既定構成では ポインター圧縮 を使い、予約したアドレス範囲内の位置を 32 ビットのハンドルで表します。大量のノードや型が持つ参照を小さくすることで、メモリ使用量を減らす設計です。通常のポインターを使う設定もあります。 この実装には unsafe も含まれます。いつデータを解放し、どのスレッドで更新できるかは実装側で確認する必要があり、監査やチェック方法は Rust 実装上のルール にも文書化されています。 以前の Rust 移植で難しかった部分に対して、こうした設計を用意しているわけですね。個人的には言語を置き換えるだけでなく、メモリの扱いまで変えているところが面白いと感じました。 なお、互換性についてのドキュメント では、完全な互換性を安定版の目標としています。通常モードと高速化用の fast モードも区別され、後者では型チェックの順序などの変更に伴うエラーの差が明記されています。 tsc-rs Theo Browne 氏の ts-rust は npm では tsc-rs という名前で提供されています。こちらも Go 版を Rust に移植し、コンパイラや言語サーバーを対象にした実験的なリリースです。 特徴的なのは、開発者自身が LLM による移植の実験として説明していることです。README には移植の試行やモデルを変えた経緯も書かれています。 ちなみに冒頭で紹介した 10 月 20 日の発表では、tsrs と tsc-rs を単一のパッケージに統合するため協力していることにも触れています。別々の実装として増え続ける、という状況でもなさそうです。 bun check Bun 1.4.3 は 2026 年 10 月 10 日に bun check を公開しました。Go 版の移植を組み込み、tsconfig.json を読んで型チェックします。Bun 本体も 1.4 で Zig から Rust への移行を発表しています。 型チェックのドキュメント にあるように、bun run --check、bun test --check、bun build --check と普段の開発コマンドに組み込めるのが特徴です。型エラーがあれば実行やテストの前に止められます。 ただし、bun check は JavaScript や .d.ts の出力、言語サーバーを提供しません。エディターは別の TypeScript を使うため、診断結果をそろえるには、Bun が対応するバージョンを確認してエディター側も合わせます。 tsz tsz は、型チェック、JavaScript の生成、言語サービスを扱う Rust 製の互換実装です。アーキテクチャの説明 では、Checker と Solver を分ける設計を紹介しています。 Checker はソースコードのどこを調べ、どの位置にエラーを報告するかを扱います。Solver は型推論や、ある型の値を別の型に代入できるかの判定を担当します。同じ構造の型を共有し、TypeId で参照する設計も説明されています。 Go 版の関数との対応を重視する tsrs と比べると、型を判定する処理をどう分けるかに違いがあります。ただし、既存処理の整理は続いており、すべての処理が完全に分離されているわけではありません。公式との互換性についての課題 も公開されています。 tsc-ts Rust 以外の言語による別実装として、tsc-ts も紹介します。Go 製 TypeScript 7.0.2 を TypeScript に移植した実験的なコンパイラで、そのコードを scriptc でコンパイルし、ネイティブ実行ファイルとして提供します。 TypeScript に戻したのに速くなる、というのが少し不思議に見えますが、従来とは実行方法が違います。JavaScript にして動かす代わりに、コンパイラ自身を機械語にしています。 scriptc は型情報を使って値の表現を決め、LLVM でコードを生成します。コンパイルできる範囲には制限があり、未対応の処理は報告されます。 なぜこの時期に別実装が相次いだのか tsrs や tsc-rs には、Go 版という移植元と公開テストがあります。言語の挙動やアルゴリズムを一から設計するのとは出発点が違い、元の実装と比較しながら開発できます。こうした蓄積も後発の移植を支えているのではないかと考えています。 移植に使う手段も変わっています。tsc-rs は LLM による移植の実験で、先ほどの tsrs の発表でも AI が大規模な移植を進められるようになったことを背景に挙げています。ただし、Rust 側のメモリ管理の設計や、元と同じエラーを出すかの確認は残ります。 さらに、各実装が目指す使い方も異なります。tsrs は速度に加えて、メモリ使用量や WebAssembly(Wasm)での実行を重視しています。bun check は実行・テスト・ビルドと型チェックをまとめています。こうした用途に合わせてメモリ管理や組み込み方を変えることも、別実装を作る動機になっていると感じました。 公開ベンチマークを見てみる 公開されている測定結果をいくつか載せます。 測定環境や対象コードが異なるので、別々の表の秒数を比べて順位は付けられません。処理系による違いに加えて、同じ処理系でも対象コードによって差が出る点を見ていただければと思います。 tsgo・tsrs・bun check の比較 tsrs の 公開ベンチマーク から、型チェックを並列に行うワーカー数を 4 にそろえた測定を抜粋しました。これは 0.19.0 beta より前に行われた測定の記録です。 対象 tsgo tsrs bun check VS Code 10.94 秒 2.21 秒 4.48 秒 Playwright 1.23 秒 0.24 秒 0.50 秒 16 vCPU / 63 GB RAM の AMD EPYC / Linux で、--noEmit による型チェックを測っています。tsgo は 7.0.2、tsrs はコミット d57703ab5f07 の PGO(事前に集めた実行プロファイルを使う最適化)適用ビルド、Bun は 1.4.4-canary.1+5f5799ed1 です。中央値を使っていますが、測定回数は tsgo が 3 回、ほかは 10 回です。 この結果では tsrs が速いものの、移植元は 7.1 の開発版(b85298b6)で、tsgo の 7.0.2 とはバージョンが違います。VS Code ではエラー数にも差があり、公開資料では対応する Go の開発版と照らして説明しています。速度と互換性は分けて見る必要がありそうです。 tsc-rs の比較 tsc-rs の開発元も実際のアプリを使った 測定結果 を公開しています。入力コードや各回の記録は、測定データ から確認できます。 対象 tsgo tsc-rs tsc-rs --fast VS Code 9.57 秒 4.81 秒 2.64 秒 Sentry 13.49 秒 5.30 秒 2.97 秒 Playwright 0.82 秒 0.41 秒 0.33 秒 測定日は 2026 年 10 月 13 日で、16 AMD EPYC vCPU / Linux、ウォームアップ 1 回後の 5 回の中央値です。--noEmit --incremental false を使い、並列数は各ツールのデフォルトのままです。 tsc-rs は 0.6.0 の release PGO ビルドで、BOLT は使っていません。比較する Go 版は移植元と同じ 7.1.0-dev.20261008.1 です。この測定では、通常モードと fast モードの両方で Go 版と診断結果が一致したと報告されています。互換性の確認はこの測定対象の範囲です。 tsz は対象によって結果が変わる tsz の 公開結果 では、速いケースと遅いケースの両方が見られます。 対象 tsgo tsz RxJS 129 ミリ秒 51 ミリ秒 ts-essentials 100 ミリ秒 109 ミリ秒 これは 2026 年 10 月 25 日 04:55 UTC 生成の一覧ページに掲載された値です。tsz はコミット 0b4641a、PGO なしの release ビルドです。GitHub Actions の Linux 上で、Intel Xeon Platinum 8370C、4 CPU / 15.6 GiB RAM という条件の測定です。 同じ一覧には、Zod では tsz の型チェックに Go 版の約 19.4 倍の時間がかかることや、型チェックが完了しなかったケースも記載されています。なお、一覧ページだけでは Go 版の厳密なコミットや測定回数を確認できませんでした。一覧と個別ページの表示値にも差があるため、ここでは一覧に掲載された結果の紹介にとどめます。 tsc-ts の公開された測定例 tsc-ts については、enyst 氏が手順と記録を公開しているベンチマーク がありました。以下は GitHub Actions 上の結果です。 対象 従来の tsc tsgo tsc-ts Canvas 29.071 秒 5.452 秒 3.770 秒 SDK の TypeScript クライアント 2.253 秒 0.319 秒 0.334 秒 SmolPaws server 2.647 秒 0.898 秒 0.546 秒 測定日は 2026 年 10 月 17〜18 日で、Ubuntu 24.04 / x86-64、4 論理 CPU のランナー上の結果です。--noEmit の新規プロセスを起動し、ウォームアップ後に 5 回ずつ交互に測った中央値を使っています。各プロジェクトは別ジョブで、並列数は各ツールのデフォルトのままです。 tsc-ts は 0.0.2、Go 版は @typescript/native-preview@7.0.0-dev.20260707.2 という正式公開前のプレビューです。従来の tsc は Canvas と SDK が 6.0.3、server が 5.9.3 です。Canvas では baseUrl を削除し、paths に相当する指定を追加した共通設定を 3 ツールで使っています。互換性の確認は、意図的に追加した少数の型エラーに対するものです。 Canvas と server では tsc-ts が速く、SDK では Go 版が少し速い結果でした。比較対象の Go 版が古いため、現在の公式実装にも同じ差があるとは言えませんが、TypeScript 製のコンパイラをネイティブで動かす例として興味深いです。 「何倍速い」の前提も確認したい 公式実装も改善を続けています。10 月 20 日公開の TypeScript 7.1 Beta では、4 ワーカー、--noEmit の条件で、7.0 から 1.2〜1.9 倍の高速化が報告されています。 先ほどの tsrs の発表でも、一部の最適化が公式実装に取り込まれたと説明されています。Rust と Go の実装が別々に速くなるだけでなく、改善を共有しているのも面白いところです。 おわりに 今回調べてみて、Go 版や公開テストを利用でき、AI で移植を進める手段も増えたことが、別実装を作りやすくしているのではないかと感じました。後発の実装は公式の成果を使いながら、用途に合わせてメモリ管理や実行方法を工夫しています。 エディターとの連携や CI の高速化、ブラウザー内での実行など、用途によって選ぶ処理系も変わっていくのではないかと思います。 一方で、公式の変更を追って互換性を保ち続ける作業は残ります。個人的には速度だけでなく、各実装がどのように変更を取り込み、協力・統合していくのかも引き続き追っていきたいと思います。 脚注 「約 3 か月後」は、7.0 の正式公開から、10 月に型チェック機能の公開や協力体制の発表が続いた時期までの間隔です。各プロジェクトの開発開始日や、移植にかかった期間を示すものではありません。 ↩︎

  4. Why software is converging on Rust in the age of AI For about a decade, "rewrite it in Rust" was a joke. It was what an excitable stranger posted under every bug report in a C++ project, and the reply was some polite version of no, because rewrites took years and broke things that already worked. Dropbox spent years moving its sync engine from Python to Rust, and its own write-up warns that in many companies it would have been a terrible idea. Over a few months in 2026, one large project after another did it anyway, and none of them took years. In February, Andreas Kling moved the JavaScript engine of the Ladybird browser from C++ to Rust in about two weeks, and it passed all 52,898 tests of the official JavaScript conformance suite with byte-for-byte identical output. In May, Bun, the JavaScript runtime, went from 535,000 lines of Zig to Rust in 11 days, with up to 64 Claude agents working through roughly 16,000 compiler errors. Meta merged a Rust version of the React Compiler in June and describes most of it as written by AI. By September, a single engineer at GitHub directing up to 22 agents had turned the TypeScript runtime behind Copilot into 832,000 lines of Rust. And in October, a solo developer named Brandon Thomas published open-source Rust clones of Photoshop, Illustrator and Premiere, built by directing Claude, and collected 70,000 GitHub stars in three weeks. ForrestKnight's video on this has the best one-line summary I've seen: Rust didn't change this year, the cost of rewriting did. It's 25 minutes and worth it. {% embed youtube.com %} He's right, and it's half the answer. Cheap rewrites explain why teams are switching languages now. They don't explain why so many are switching to the same one, when Go, Zig and modern C++ were all available. The other half is about who reads the code. When an agent produces a thousand lines an hour, nobody reviews all of it, so whatever the compiler checks for free ends up carrying most of the weight. Rust's compiler checks more than any other mainstream language's, and agents turn out to be good at taking its feedback. Rewrites used to take years Choosing a language used to be one of the few permanent decisions in a project. You could swap a database or a cloud provider, but changing languages meant pausing feature work for a year or more while a team re-implemented everything and chased down the behaviour that shifted along the way. Jarred Sumner, who created Bun, described language choice for a project like his as having been "a one-way decision" until very recently. The fish shell is a good picture of the old cost. Its move from C++ to Rust, finished in 2024 without AI, took almost two years and more than 200 contributors to port 57,000 lines. Compare that with this year: | Port | From | Size | Time | Who | |---|---|---|---|---| | fish shell (no AI) | C++ | 57K lines | almost 2 years | 200+ contributors | | Ladybird's LibJS | C++ | ~25K lines of Rust | ~2 weeks | Andreas Kling, with Claude Code and Codex | | Bun | Zig | 535K lines | 11 days | One engineer, up to 64 agents at once | | Copilot runtime | TypeScript | ~430K lines in, ~832K lines of Rust out | 14.5 weeks | Mostly one engineer, up to 22 subagents | | React Compiler | TypeScript | +123K lines | merged June 2026 | Meta, "majority coded by AI" | You can see the result on GitHub. A project's language bar is computed from its source files, and Bun's now looks like this: ![GitHub's language breakdown for the oven-sh/bun repository: Rust 76.2%, C++ 13.6%, TypeScript 7.2%, C 1.5%, JavaScript 1.2%, Shell 0.2%. There is no Zig in the list.](dheerajakula.dev "Bun's repository in October 2026. A year ago the biggest slice of that bar was Zig. The C++ is JavaScriptCore glue that stays.") These numbers deserve some care. Bun's port cost around $165,000 in API usage and needed one very experienced engineer watching it the whole time. Sumner's own estimate for doing it by hand was three engineers for about a year. Kling describes Ladybird's port as "human-directed, not autonomous". And Bun is owned by Anthropic, whose models did the work, so its write-up is partly a showcase. Even so, the cost of switching languages has fallen by something like two orders of magnitude. One thing shows up in every port that worked: a test suite that doesn't care what language the code is in. Bun's tests are TypeScript that drives the runtime from outside, so the same 60,000 tests could judge the Zig version and the Rust version. Ladybird had test262, the official JavaScript conformance suite. Agents translate quickly, and the test suite is what tells you whether the translation is right. The big players are betting on the same idea. In December 2025, Microsoft Distinguished Engineer Galen Hunt set a goal of "1 engineer, 1 month, 1 million lines of code" for AI-driven translation of C and C++ into Rust, then clarified that Windows isn't being rewritten and the work is a research project. DARPA runs a program called TRACTOR, short for Translating All C to Rust, with the same aim. Why teams wanted to rewrite: speed and memory Cheap rewrites only matter if someone wants one, and the usual reason is performance. Rust compiles to native machine code, like C and C++, and it has no garbage collector: memory is freed the moment its owner goes away, not in periodic sweeps. That raises throughput and keeps latency flat instead of spiking whenever a collector runs. The published before-and-after numbers are large when the old code was garbage-collected or interpreted: | Project | Compared with | Result | |---|---|---| | Cloudflare's core proxy | its NGINX and LuaJIT predecessor | 10 ms faster median response, on under half the CPU and well under half the memory | | AWS Aurora DSQL | a tuned Kotlin version | about 10x the throughput: roughly 30,000 transactions per second against 2,000 to 3,000 | | Discord's Read States service | the Go version | the latency spikes Go's garbage collector caused every few minutes went away | | vLLM's frontend | the Python frontend | median time to first token from 166 ms to 50.5 ms | | Vite 8's Rolldown bundler | Rollup, in JavaScript | Linear's production build from 46 seconds to 6 | | ty, Astral's Python type checker | Pyright | an incremental check in 4.7 ms instead of 386 ms | | GitHub's Copilot runtime | the Node version | no more Node host taking about 100 MB per SDK client | Ports from C, C++ or Zig look different, because those languages were already fast. Bun got 2 to 5% faster after its move from Zig, and Claude Code, which runs on Bun, started about 10% quicker on Linux (517 ms to 464 ms). The bigger change was memory. Building a 60-module project 2,000 times in one process used 6,745 MB on the Zig version, which leaked about 3 MB per build, and 609 MB on the Rust one. Part of the speedup from leaving JavaScript or Python comes from going native at all. Microsoft's Go port of the TypeScript compiler made full builds 8 to 12 times faster with no Rust involved. Where Rust pulls ahead of Go and the JVM is the missing garbage collector, which gives steady tail latency and a smaller memory footprint, and that's what Discord and the Aurora DSQL team said they were after. The compiler became the first reviewer Cheap rewrites move the bottleneck. GitHub's port produced 832,000 lines of production Rust and another 469,000 lines of tests in three and a half months. No one read that line by line. The checking moves to things that can keep up: the test suite, other agents set up as hostile reviewers, and the compiler. Of those, the compiler is the one that runs on every single change, in seconds, and never gets tired or changes its mind. Bun's agents used the output of cargo check, Rust's fast type and borrow check, as their to-do list. They burned down those 16,000 errors crate by crate before running a single test. Sumner explained the choice of Rust over a stricter Zig style guide in one line: compiler errors are a better feedback loop than a style guide. A style guide depends on someone noticing the violation. A compiler error stops the build. Here's what that loop looks like on a small change, with two real compiler errors in it. Press play, or step through it. > There's an interactive figure here. Try it on the original post. There's research behind this now. A 2026 paper on generative compilation tested seven models on repository-sized Rust tasks. Left alone, the models wrote Rust that didn't compile 65.9% of the time on average; the paper is frank that Rust is harder to get right on the first try than permissive languages like C. Feeding the compiler's errors back after generation cut that to 20.7%, and checking partial programs while the model was still writing cut it to 13.1%. In one setup, a frontier model went from 51.7% non-compiling to zero. What surprised me is how well agents do with Rust once that loop is in place. On SWE-bench Multilingual, a benchmark built from real GitHub issues in nine languages, the tested agent resolved Rust issues at the highest rate of any language, 58%. C and C++ came last at 29%. There are only 43 Rust tasks, so I wouldn't lean hard on the exact number, but it points the opposite way from the usual worry that agents would struggle with Rust because there's less of it to learn from. And in GitHub's Copilot port, the dreaded ownership and borrowing errors made up just 1.7% of all the compile errors the agents hit. Most were ordinary slips with names and types. What Rust's compiler catches that others don't Every typed language checks something. What matters is how many kinds of bug get turned into compile errors, and Rust covers several kinds that other languages leave to tests, sanitizers or luck. Pick a bug and see when each language finds it: > There's an interactive figure here. Try it on the original post. Memory bugs Use-after-free, double free and reading past the end of a buffer are the classic C and C++ bugs, and they're behind most serious security holes. Microsoft has said for years that about 70% of the CVEs it assigns are memory-safety bugs, and Chromium reports the same share of its high and critical bugs. In safe Rust, the ownership rules make all of these compile errors. Bun's last Zig release notes read like a catalogue of them: use-after-frees in node:zlib and node:http2, a double free in the CSS parser, leaks on error paths. Android has the longest track record. Google has written new Android code in Rust since 2019, and reported in November 2025 that memory-safety bugs had dropped below 20% of Android's vulnerabilities for the first time, down from 76% in 2019. Android's C and C++ code runs at roughly 1,000 memory-safety vulnerabilities per million lines. Its 5 million lines of Rust have had one near miss, about 0.2 per million. Rust changes also had a 4x lower rollback rate and spent 25% less time in code review, and once agents write the code, review time is the part that doesn't shrink. Data races Rust tracks which types can safely move or be shared between threads, through two traits called Send and Sync, and refuses to compile code that would share something unsafely. In TypeScript, Go or C++, a data race compiles and fails now and then in production. Daniel Szoke from Sentry built a whole talk, Why Rust is the Ideal Language for Vibe-Coding, around this: the Rust compiler names the exact type that can't cross threads, which points the agent straight at the fix. Forgotten cases A match on an enum has to handle every variant. Add a new status, and every match that doesn't handle it becomes a compile error with its location. Agents are good at the change you asked for and worse at finding the six other places it should ripple to. Rust turns that hunt into a list. Failure paths Rust has no exceptions and no null. A function that can fail returns a Result, a value that might be missing is an Option, and the compiler makes the caller handle both before touching what's inside. You can tell from a function's signature whether it can fail, which is most of what a reviewer wants to know. Error messages a model can act on rustc errors give the exact location, explain why, and often include a suggested edit. The same output comes as JSON, with each suggestion marked by how safe it is to apply automatically, and cargo fix applies the safe ones. The Rust project built this for people years before coding agents existed. For a model in a loop, it's close to a set of instructions. One toolchain everywhere Every Rust project builds with cargo, tests with cargo test, lints with cargo clippy and formats with rustfmt. An agent that knows one Rust project knows how to build and check all of them. In C++, the build system alone can take a new human days to figure out. Rust had all of these properties in 2018, and for years the effort of satisfying its compiler kept a lot of people away. That effort is exactly the part agents are happy to grind through, and the payoff, code that's been checked before anyone reads it, is exactly what you need when nobody can read it all. Niko Matsakis, a longtime lead of the Rust language team, makes a similar case: Rust's old promise that code which compiles usually works matters more when an agent is the one compiling. Rust is spreading well past the server The usual story about Rust is infrastructure, like Cloudflare's proxy and AWS's databases, and that's where it paid off first. But over the last two years Rust has moved into places that have nothing to do with servers. Click around: > There's an interactive figure here. Try it on the original post. Browsers Browsers parse untrusted input from every website on earth, so they were early adopters. Firefox has had Rust inside since its CSS engine, Stylo, shipped in 2017, and its WebGPU support is built on wgpu, a Rust graphics library. Chrome replaced the FreeType font library with a Rust font stack called Skrifa and removed FreeType from Blink entirely in Chrome 145. Chromium's JSON parser is Rust too, because that's what makes it safe to run on untrusted input at high privilege. The most interesting browser story is Ladybird, a new engine built from scratch by a nonprofit. In 2024 it picked Swift as its future language. In February 2026 it dropped Swift, because the C++ interop "never quite got there", and moved to Rust, starting with the JavaScript engine that agents helped port in two weeks. ![The Ladybird browser rendering the Wikipedia article about Ladybird, with tabs, an address bar and the page's infobox showing the Ladybird logo.](dheerajakula.dev "Ladybird showing its own Wikipedia page. Its JavaScript engine is now Rust. Screenshot from ladybird.org.") Operating systems Kernels are where a single memory bug becomes a security hole, and Rust is now inside Windows, Linux and Android. Windows has shipped Rust in the kernel since 2024, starting with the code that handles GDI regions. The Linux kernel declared its Rust experiment over in December 2025, which means Rust is now a permanent part of Linux. Android's Binder driver has a Rust rewrite in mainline, and there are Rust GPU drivers for Arm Mali, Apple silicon and, still early, NVIDIA. In Android itself, new Rust code overtook new C++ in 2025. Mark Russinovich's RustConf 2025 keynote is the best account of how this went inside Microsoft, including a bug that would have been a privilege escalation in C++ and became a clean, reproducible crash in Rust: {% embed youtube.com %} Above the kernel, System76 shipped COSMIC, a whole desktop environment written in Rust from the ground up, as the default in Pop!_OS 24.04. ![Three COSMIC desktop screenshots side by side: a dark wallpaper of a lantern-lit street, a beach seen from above with a dock along the bottom, and a dark desktop with widget panels and a weather applet.](dheerajakula.dev "COSMIC, System76's desktop environment, written in Rust from the ground up. Screenshots from system76.com.") Graphics, 3D and creative tools This is the part I know best, because I'm building in it. The 3D editor I'm working on is Rust compiled to WebAssembly, drawing with WebGPU from a worker in a browser tab. Two years ago that would have been an odd choice. Now it's fairly ordinary. The biggest surprise of the year was PhotoCraft. In October, Brandon Thomas published PhotoCraft, an open-source Photoshop alternative written in Rust, along with sibling projects for Illustrator, Premiere, Lightroom and other Adobe apps, built mostly by directing Claude agents. It has layers, adjustment curves, masks and 67 tools, and it re-saves Photoshop files that match the originals in 307 of 309 test cases. ![PhotoCraft open on Hokusai's The Great Wave off Kanagawa, with a Photoshop-style layout: a tool column on the left, the image in the centre, and a Curves adjustment panel and layer stack on the right.](dheerajakula.dev "PhotoCraft editing The Great Wave off Kanagawa (public domain) with a Curves adjustment layer. The whole app is Rust. Screenshot from the PhotoCraft repository, MIT/Apache-2.0.") It's honest about being early alpha and not yet a tool for professional work, and whether its "clean-room" process holds up legally is an open question. Adobe wasn't involved and hasn't commented. But a few years ago, a credible Photoshop alternative would have needed a funded company and years of work. Graphite is another creative tool in the same spirit: a node-based vector and raster editor written in Rust, still in alpha, that mixes layers with procedural, generative editing: ![Graphite's editor window showing a flat-colour illustration of a woman's face in profile surrounded by procedurally generated polka dots, with a properties panel on the right.](dheerajakula.dev "Graphite, a procedural 2D editor written in Rust. Screenshot from graphite.art.") Zed, the code editor, reached 1.0 this year on GPUI, its own GPU-rendered UI framework in Rust, with agents built in. Bevy is the most active Rust game engine, and Vello renders 2D vector graphics entirely on the GPU. Figma moved its multiplayer server to Rust back in 2018. ![The Zed editor with an agent thread on the left discussing a memory optimisation, and a diff of Rust code on the right with removed lines in red and added lines in green.](dheerajakula.dev "Zed, a code editor written in Rust, reviewing an agent's change to Rust code. Screenshot from zed.dev.") Developer tools Python's tooling is now largely Rust underneath. Astral's uv, ruff and ty, whose company joined OpenAI in March, are among the most downloaded packages in the ecosystem. pydantic-core, the Rust engine inside Pydantic, was downloaded about 808 million times in a single 30-day stretch this autumn, roughly twice as often as pip itself. JavaScript went the same way: Turbopack is the default bundler in Next.js 16, and Vite 8 bundles with Rolldown, both written in Rust. The coding agents themselves are moving too. OpenAI's Codex CLI left Node for Rust to get a dependency-free install and native sandboxing, and is now about 97% Rust. GitHub's Copilot runtime followed, xAI's Grok Build shipped in Rust from day one, and Warp, the terminal that has turned into an agent workspace, has been written in Rust and drawn on the GPU from the start. ![Warp's agent workspace: on the left, an agent's step-by-step log of implementing a feature and running tests; on the right, a diff of a Rust file with an inline review comment.](dheerajakula.dev "Warp, written in Rust, with an agent working on Rust code and a review comment on the diff. Image from Warp's GitHub repository.") The AI stack The code around AI models has been picking up Rust for years. Hugging Face's tokenizers library and the safetensors file format are Rust, and almost every model download pulls in both. In May, vLLM, the most popular open-source inference server, merged an optional Rust frontend because the Python frontend had become the CPU bottleneck. The proposal argued that, with coding agents around, the old objection that contributors can't read Rust is "minimized". Where Rust isn't winning Rust has made little headway in three areas that matter a lot, GPU kernels, research code and games, and in a few others another language won on purpose. GPU kernels The code that actually runs AI models is still CUDA C++, plus more and more Python. NVIDIA's recent bets are Python-first: cuTile Python, the CuTe DSL in CUTLASS 4, and native CUDA Python. NVIDIA did announce official CUDA Rust in September, with two tracks called cuda-oxide and cutile-rs, but by its own description neither is production-ready, and it calls CUDA C++ and Python the mature toolchains. The community Rust-CUDA project came back to life in 2025 after three dormant years and is still early. If you write kernels for a living, Rust isn't on your list yet. Research and training Research lives in Python because researchers iterate in notebooks, and PyTorch and JAX are Python on top of C++ and CUDA. The biggest inference engines are Python or C++ as well: llama.cpp has about 132,000 GitHub stars and vLLM about 95,000, while the leading Rust ML framework, candle, has about 21,000. Hugging Face's TGI, the best-known inference server with a Rust core, is now in maintenance mode, and its own docs point people to vLLM and SGLang. So far Rust takes the always-on pieces around the model (the server frontend, the tokenizer, the file format), Python keeps the places where people experiment, and CUDA keeps the maths. Games Unreal is C++, Unity is C#, and Bevy is on version 0.20. LogLog Games wrote a widely shared post, Leaving Rust gamedev after 3 years, after 100,000 lines: games need fast iteration and hot reloading, and the borrow checker kept forcing refactors when they wanted to try ideas. Agents make refactors cheaper, but they don't change how much game development values trying ten things in an afternoon. Places where another language won The most useful counterexample is TypeScript itself. When Microsoft rewrote the TypeScript compiler for speed, it picked Go, and TypeScript 7 shipped this year with full builds 8 to 12 times faster. The team's reasons were specific: they wanted a line-by-line port, Go's style is close to the existing code, garbage collection suits a compiler, and the compiler's syntax trees are full of cycles, which Rust's ownership model makes awkward. Zig keeps strong projects like Ghostty and TigerBeetle, whose authors chose it deliberately. Go still runs cloud infrastructure, from Kubernetes to Terraform. And Armin Ronacher, who knows Rust well, argues that Go and TypeScript are better languages for agents, partly because agents struggle with Rust's macros. Bugs the compiler can't see Rust's checks have edges, and agents find them too. Bun's port had 19 regressions, all in code that compiled. In one, a debug_assert! wrapped a call that had a side effect, so release builds quietly skipped the call and hot reloading broke. Cloudflare's November 2025 outage was a Rust proxy calling unwrap() on an error it had never seen before, which crashes by design. Check Point found a Rust panic in the Windows kernel that blue-screened the machine. Code at the boundary with C stays unsafe, around 4% of both Bun's and Android's Rust, and the Linux kernel's first Rust CVE came from an unsafe block. Rust also compiles slowly: in the project's 2025 survey, 55% of respondents wait more than 10 seconds for a rebuild, and an agent running hundreds of builds feels that too. A panic is a better way to fail than memory corruption, and a crash is better than a silent security hole. Reviewers still have to read for logic errors, panics and unsafe; Rust just makes the list shorter. Where this is heading Put together, the stack seems to be sorting itself into layers, and no single language gets all of them. Code that runs all day, handles untrusted input, and has to be both correct and fast is moving to Rust: runtimes, browser engines, kernel drivers, toolchains, agent harnesses, inference frontends, creative tools. Agents are speeding that up twice, by making the port affordable and by doing the part of Rust people found tedious. Code where people are still exploring, like research, prototypes, scripts and UI glue, stays in Python and TypeScript, where the feedback loop is a person trying things. GPU kernels stay wherever NVIDIA's tools are best, which today means C++ and Python. And the huge body of existing C and C++ shrinks slowly, as efforts like Microsoft's research project and DARPA's TRACTOR mature. That's my bet for the next five years, and parts of it could be wrong. If NVIDIA's Rust tracks mature, kernels could follow. If agents get much better at the first draft, the compiler's job as a reviewer matters less. But the direction over the last two years has been consistent, and it sped up in the last one. Try it on your own agent If you run coding agents on a codebase, a few changes help in any language and help most in Rust: 1. Put cargo check in the agent's inner loop, and cargo clippy -- -D warnings plus cargo test before it says it's done. The compiler is the cheapest reviewer you have. 2. Turn on the Clippy lints that catch agent shortcuts: unwrap_used and expect_used outside tests, and undocumented_unsafe_blocks so every unsafe needs a // SAFETY: comment. 3. Before porting anything, make sure the test suite runs from outside the code, in another language or as a black box. That's what made Bun's and Ladybird's ports checkable. 4. Use a second agent with fresh context as a hostile reviewer that only sees the diff and is told to assume it's wrong. Bun ran two of them on every change. 5. Read the diff yourself where the compiler can't help: logic, unsafe, unwrap, and anything that touches money or permissions. That last step needs you to read Rust. If you don't yet, A Rust primer for humans goes from hello world to reviewing an agent's Rust diff, with Python alongside. For what's on the other side of the pull request, there's what an agent harness is and a walk through OpenCode's source. For the other side of the argument, Cliff Biffle's Common Pitfalls of Rewriting Things in Rust is worth an hour, and for where Linux is heading, so is Greg Kroah-Hartman's Untrusted data in Linux. --- *I'm Dheeraj, a software engineer at Nutanix Enterprise AI working on agent harnesses and developer tools. I write up the problems that took me too long to work out. More at dheerajakula.dev.*

  5. DEV · · @alanwest

    Cloudflare's Rust TypeScript (tsrs) vs tsgo: The 8x Claim Is Closer to 3x Microsoft spent most of 2025 explaining why the native TypeScript compiler would be written in Go and not Rust. TypeScript 7.0 (tsgo) shipped on July 8, 2026. On October 20, Cloudflare published tsrs, a port of TypeScript to Rust, and says it is "up to 8x faster" than TypeScript 7.0 with "~70% less memory". The fairest comparison in the published numbers puts it closer to 3x. So we now have two native TypeScript compilers, and only one of them is tsc. I've read the Cloudflare post, the tsrs repo and its benchmark file, Microsoft's announcements and the 207-comment Hacker News thread. My view after all that: both companies picked the right language for their own problem, your CI gate should stay on tsgo, and the thing that worries me is tsrs turning into a second tsc. What tsrs actually is The tsrs repo lives under the Oxc project. It describes itself as a Rust port of the TypeScript 7 type checker, meant as a drop-in replacement for the TypeScript 7 compiler and language server, with "100% conformance" as the goal. A few facts that matter more than the headline number: - The README credits Max Schwenk with the port, and Owner.com sponsored the initial work. Cloudflare and VoidZero are backing it, and Cloudflare says Theo Browne's tsc-rs will be merged with it into one package. - It tracks the main branch of microsoft/TypeScript (the benchmarks use a 7.1-dev commit) and is aiming for a stable release against TypeScript 7.1. Releases so far are 0.x: v0.18.0 landed on GitHub on October 19, and npm ships it as 0.18.0-ts7.1.0-dev.20260929. - License is Apache-2.0. The README says it is not affiliated with or endorsed by Microsoft. - Pull requests are closed. Issues with minimal repros are welcome. - There's a Wasm build, @tsrs/wasm, targeting wasm32-wasip1. - Known gaps: --init and --showConfig aren't supported, and the benchmark file flags at least one real tsrs bug (same error count as tsgo on drizzle-orm, different locations). Cloudflare's post insists "we are not forking TypeScript". It also says "frontier AI models can now do large-scale ports almost independently in a matter of days" and that it will apply the same process it uses for Vinext, where the Next.js test suite runs against Vinext every night, to keep tsrs in sync with upstream, with an agent reviewing upstream changes each morning and opening tracking issues. That's the whole maintenance plan: agents and a test suite. Why Microsoft picked Go Ryan Cavanaugh's "Why Go?" discussion from March 2025 is still the best primary source. The main reasons: - The new codebase had to stay "as compatible as possible, both in terms of semantics and in terms of code structure", because the team would maintain both codebases for a while. - "Idiomatic Go strongly resembles the existing coding patterns of the TypeScript codebase." - Go offers "excellent control of memory layout and allocation", and "the downsides of a GC aren't particularly salient in our codebase." - The admitted weakness: "Go's in-proc JS interop story is not as good as some of its alternatives." That plan worked. The TypeScript 7.0 announcement reports VS Code going from 125.7s on TypeScript 6 to 10.6s on TypeScript 7, and 8x to 12x speedups on full builds generally. 7.0 is built to match 6.0's type-checking and command-line behavior, which is exactly what you want from a compiler that so many CI pipelines depend on. Why Cloudflare picked Rust Cloudflare's argument for Rust is mostly about Wasm: "When you compile a Go program to Wasm, you have to compile Go's garbage collector along with it." Rust's memory management model "enables straightforward memory interactions between a WebAssembly guest and host." If you want a type checker inside a Worker, inside a browser playground, or inside Oxlint as a library, that is a real difference. The most important comment in the HN thread comes from Daniel Rosenwasser of the TypeScript team. "We even did our own Rust vibe port", he wrote, and the team suspects "a lot of the speed does not just come down to language choice." Some of what makes TS 7.1 faster "actually comes from optimizations discovered in tsrs": work done more lazily, changed allocation patterns, experiments with struct layouts. Cloudflare's post says the same thing from the other side: some 7.1 optimizations "were upstreamed from tsrs". So the Go vs Rust argument is mostly a proxy war. Go was the right call for a faithful port by the people who own the language. Rust is the right call if you want to embed the checker somewhere a GC is a liability. tsrs vs tsgo benchmarks: where 8x becomes 3x These are the published figures. None of them are mine; each row comes from the source named in it. | Source | Project | tsgo (TS 7.x) | tsrs | |---|---|---|---| | Cloudflare blog, same worker count | general claim | baseline (7.0) | up to 8x faster, ~70% less memory | | Cloudflare blog | TS 7.1 beta vs 7.0 | up to 2.5x faster | n/a | | bench/RESULTS.md, defaults, 16 vCPU | vscode | 11.10 s, 7.48 GiB | 0.82 s, 2.10 GiB | | same | mui-docs | 12.66 s, 7.39 GiB | 0.75 s, 1.60 GiB | | same, watch mode, 8 vCPU | vscode edit-to-report | ~1.2–1.8 s | ~31–34 ms | | HN, quoting the blog charts | vscode vs TS 7.1 nightly | 5.28 s | 1.73 s | The 13.55x vscode result in RESULTS.md runs tsgo at its default 4 checkers and tsrs at 16 threads on a 16-vCPU box, compares against typescript@7.0.2 while tsrs ports a 7.1-dev commit, and uses tsrs's PGO-optimized build. When Cloudflare equalizes worker counts, the claim drops to "up to 8x". When you compare against the 7.1 nightly instead of 7.0, the VS Code gap is about 3x. That's still a big result, and 3x is the number I'd quote. The watch-mode number is the one I'd actually care about. Edit-to-diagnostic in tens of milliseconds instead of over a second is something you feel in an editor all day. A 12-second CI check becoming 1 second mostly isn't, and commenter pier25 has a point: for most projects tsgo is "already plenty fast." What breaks in TypeScript 7 and in tsrs tsgo (TypeScript 7.0) ships without a programmatic API. The announcement says so directly: "it does not ship with an API." typescript-eslint, Volar, Vue, Svelte, Astro and MDX tooling still need 6.0. Microsoft's workaround is @typescript/typescript6, which gives you a tsc6 binary and the old API. A new, different API is expected in 7.1. tsrs has no stable release, only 0.x builds against a 7.1 dev snapshot. --init and --showConfig aren't supported. The README also warns that tsgo itself can produce diagnostics and traces in a different order depending on parallel parse, resolution and check order, so a naive diff between the two will be noisy even when both are right. tsrs canonicalizes some of that by default; tsgo doesn't. And keeping a port in sync with agents that track upstream is a plan nobody has seen survive a major TypeScript release yet. What to put in CI today My recommendation for a real project in October 2026: # 1. The gate stays official npm install -D typescript@7 # 2. Keep the 6.0 API for typescript-eslint and friends npm install -D typescript6@npm:@typescript/typescript6 # 3. Before chasing another compiler, tune the one you have npx tsc --noEmit --checkers 8 # 4. Try tsrs as a non-blocking job and compare results pnpm add -D tsrs pnpm exec tsrs --noEmit --checkerAssignment go --noLazyMembers The two tsrs flags in step 4 are the ones its README recommends for the closest comparison with tsgo. If the error lists match on your codebase for a few weeks, you have a fast pre-check. Your merge gate should stay on tsc. Oxlint, Biome, swc: who gets type-aware linting Oxc wins, obviously. Cloudflare names type-aware lint rules in Oxlint as a use case, and the repo lives under oxc-project. Rust linters have spent years approximating types without a real checker, and an in-process Rust type checker fixes that for whoever gets to link against it. Neither Biome nor swc is mentioned in the Cloudflare post. If tsrs ends up as a crate with a clean API that Biome, swc, Rolldown and anyone else can depend on, the whole Rust JS toolchain gets real types. If it ends up as "the oxc type checker" with its own CLI, its own LSP and its own subtly different diagnostics, we get the Babel-vs-tsc split all over again, this time at the type level, where differences hurt more. My take is that tsrs is good news as a library and bad news as a rival tsc. The CLI and LSP are what make it look like a rival. The Wasm build and the embeddable checker are the parts I'd want, and I hope that's where the effort goes. Did Microsoft get it right with Go, or did Cloudflare just prove Rust was the better call all along? And would you put a community port in your CI gate if it were 3x faster? Tell me why I'm wrong. Sources - Tsrs: A Port of TypeScript to Rust (Cloudflare) - oxc-project/tsrs README and bench/RESULTS.md - Hacker News discussion - Why Go? (microsoft/typescript-go #411) - A 10x Faster TypeScript (Microsoft, March 2025) - Announcing TypeScript 7.0 (Microsoft)

  6. There are plenty of CoPilot contributions on tsgo repo, forgotten which company Typescript development is sponsored by? typescript-go#4313

  7. TypeScript Is Unsound on Purpose: 7 Programs That Type-Check, Then Crash - Book: The TypeScript Type System - Also by me: The TypeScript Library — the 5-book collection - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- Non-goal number three on the TypeScript Design Goals page reads: "Apply a sound or 'provably correct' type system. Instead, strike a balance between correctness and productivity." The team wrote that down on purpose. The handbook says the same thing in its own words. The type compatibility chapter has a section called "A Note on Soundness" that says TypeScript "allows certain operations that can't be known at compile-time to be safe," and that the places where it does so "were carefully considered." So a green tsc --strict run tells you your code stayed inside the trade-offs the team picked, and those trade-offs include programs that throw a TypeError. Below are seven programs that compile under --strict and fail at runtime, the rule that lets each one through, and what closes it, where something does. Every rule is linked to the doc that states it, so you can paste any snippet into the TypeScript playground and check it yourself. What --strict covers, and what it skips The strict flag turns on the strict family: strictNullChecks, strictFunctionTypes, noImplicitAny, useUnknownInCatchVariables and five more. Two checks that matter below are not in that family: noUncheckedIndexedAccess and exactOptionalPropertyTypes. The TypeScript 4.1 release notes say the index check "is not automatically enabled by the strict flag" because "it might be noisy for a lot of code." Every snippet here assumes --strict and nothing else. One of them dies the moment you add a single line to tsconfig.json. The rest need a pattern, a lint rule, or a runtime check, because no flag exists for them. Variance: the array and the method 1. A cat in the dog array class Animal { name = "animal"; } class Dog extends Animal { bark() { return "woof"; } } class Cat extends Animal { meow() { return "meow"; } } function adoptCat(shelter: Animal[]) { shelter.push(new Cat()); } const kennel: Dog[] = [new Dog()]; adoptCat(kennel); for (const dog of kennel) dog.bark(); // TypeError on the Cat Dog[] is assignable to Animal[] because arrays relate covariantly: if Dog fits Animal, then Dog[] fits Animal[]. That is safe for reading and wrong for writing. push takes an Animal, the Cat goes in, and the loop calls bark on a cat. The fix: when a function only reads, type the parameter as readonly Animal[]. push doesn't exist on that type, so the write becomes a compile error. Callers can still pass a Dog[], because the handbook notes that a mutable array assigns to a ReadonlyArray but not the other way around. 2. The method that only takes strings interface Handler { handle(input: string | number): void; } const h: Handler = { handle(input: string) { console.log(input.toUpperCase()); }, }; h.handle(42); // TypeError at runtime: no toUpperCase on 42 strictFunctionTypes arrived in TypeScript 2.6 and checks function parameters contravariantly. It skips methods on purpose. The release notes say the stricter check covers all function types "except those originating in method or constructor declarations," and that "methods are excluded specifically to ensure generic classes and interfaces (such as Array<T>) continue to mostly relate covariantly." The TSConfig reference adds that the team found "a large number of inherently unsafe class hierarchies, including some in the DOM" while building the flag. So a member written in method syntax stays bivariant, and an implementation that only accepts string gets through. That same carve-out is why program 1 compiles: push is a method on Array<T>. The fix: declare the member as a property, handle: (input: string | number) => void. Now the same object literal is a compile error. The typescript-eslint rule method-signature-style enforces property style by default, and its docs state that "methods are always bivariant in their arguments, while function properties are contravariant." Promises the checker doesn't track 3. A narrowed field that changes under you interface Profile { readonly nickname: string | null; } function greet(p: Profile, onGreet: () => void) { if (p.nickname !== null) { onGreet(); console.log(p.nickname.toUpperCase()); } } const live = { nickname: "kit" as string | null }; greet(live, () => { live.nickname = null; }); // TypeError at runtime: nickname is null The object types handbook page says TypeScript "doesn't factor in whether properties on two types are readonly when checking whether those types are compatible, so readonly properties can also change via aliasing." live is mutable, greet sees it through a readonly lens, and the callback writes through the original reference. Delete readonly from Profile and the program still compiles. The modifier was never the guard. What actually lets it through is narrowing. After p.nickname !== null, the compiler keeps that narrowing across the onGreet() call. Issue #9998, "Trade-offs in Control Flow Analysis", explains why: resetting every narrowing after every call is the pessimistic option, and Anders Hejlsberg called the optimistic assumption "the best compromise." No flag covers this one. Copy the value into a local before you call out, const nick = p.nickname;, then check and use nick. A callback can't reassign your const, so that narrowing holds. And keep the asymmetry in mind: readonly arrays are checked, readonly properties are not. 4. The key that isn't there const ports: Record<string, number> = { web: 8080 }; const db = ports["db"]; // typed as number console.log(db.toFixed(0)); // TypeError at runtime: db is undefined An index signature like Record<string, number> promises a number for any string key. ports["db"] is typed number and holds undefined. Arrays behave the same way: reading past the end gives you the element type, not undefined. The fix: noUncheckedIndexedAccess. It adds undefined to every read through an index signature and every array index, so db.toFixed stops compiling until you check it. The cost is noise in index-based for loops, and the 4.1 notes point you at for...of or forEach instead. Information lost on the way up 5. The optional property that was there all along interface Base { id: string; } interface Job { id: string; retries?: number; } const raw = { id: "job-1", retries: "three" }; const base: Base = raw; // extra property: fine const job: Job = base; // missing optional: fine console.log(job.retries?.toFixed(0)); // TypeError at runtime Two legal steps. raw to Base: excess property checks run only on a fresh object literal, and the handbook's excess property checks section says assigning the object to another variable gets around them. Base to Job: Base has no retries, and a missing property satisfies an optional one. In the middle, the compiler forgets retries existed. At the end it assumes number | undefined. The string "three" isn't nullish, so ?. doesn't save you. Nothing in tsconfig closes this. exactOptionalPropertyTypes only controls whether you may write undefined into an optional property. Exact object types, which would close it, have been an open proposal in issue #12936 since December 2016. When a reader raised this case on his Effective TypeScript repo, Dan Vanderkam called it "definitely another source of unsoundness" and pointed to Item 48 of his book, which covers it. The pattern that works: don't round-trip objects through a narrower interface, and validate data at the boundary where it enters your program. Where you told the compiler to trust you 6. The assertion that skipped the check interface Account { id: string; roles: string[]; } const body = '{"id":"u1"}'; const data: unknown = JSON.parse(body); const account = data as Account; console.log(account.roles.includes("admin")); // TypeError The everyday types handbook page says type assertions "are removed by the compiler," and that "there won't be an exception or null generated if the type assertion is wrong." The cast from unknown is allowed because assertions may move to a "more specific or less specific version of a type." account.roles is undefined at runtime. The fix: replace the cast with a runtime check, either a schema validator such as Zod or a hand-written guard (see program 7 before you trust one). To ban casts outright, typescript-eslint's consistent-type-assertions accepts assertionStyle: "never", which still allows as const. 7. The type guard that lies type Cat = { kind: "cat"; meow(): string }; type Dog = { kind: "dog"; bark(): string }; function isCat(pet: Cat | Dog): pet is Cat { return "bark" in pet; // inverted check, still compiles } function adopt(): Cat | Dog { return { kind: "dog", bark: () => "woof" }; } const pet = adopt(); if (isCat(pet)) { pet.meow(); // TypeError at runtime: a Dog has no meow } A type predicate is a promise you make. The compiler doesn't prove it. The narrowing handbook page says that whenever a guard is called, TypeScript "will narrow that variable to that specific type if the original type is compatible." It reads the signature. Nothing compares the body with the claim. Dan Vanderkam's post on unsoundness makes the same point: with a guard, "you're still asserting that your type guard really guards the type." The fix: since TypeScript 5.5, the compiler can infer a predicate from the body when four conditions hold: no explicit return type or predicate annotation, a single return, no mutation of the parameter, and a boolean expression tied to a refinement of the parameter. An inferred predicate comes from the check itself, so the two can't disagree. Keep explicit predicates for checks the compiler can't follow, and give each one a unit test with a matching and a non-matching value. Does TypeScript 7 change any of this? TypeScript 7.0 shipped on July 8, 2026, with the compiler ported to Go in the typescript-go repo. The announcement says the port kept "the structure and logic of the original codebase" to keep results consistent, and that 7.0 "is made to be compatible with TypeScript 6.0's type-checking and command-line behavior." The differences it lists are elsewhere: flags deprecated in 6.0 become hard errors, strict now defaults to true, template literal types preserve Unicode code points, JSDoc support was reworked, and changing the --checkers count can surface order-dependent results. None of those touch the seven rules above, so by the team's own compatibility statement all seven still compile on 7.0. The new strict default doesn't help either, since every program here already passes --strict. The seven, side by side: | # | Hole | What closes it | |---|---|---| | 1 | Array covariance | readonly T[] parameters | | 2 | Method bivariance | Property-style members, method-signature-style | | 3 | Narrowing kept across a callback | Copy to a const before callbacks | | 4 | Unchecked index reads | noUncheckedIndexedAccess | | 5 | Optional-property widening | Validate at the boundary (no flag) | | 6 | Assertions | Runtime validation, assertionStyle: "never" | | 7 | Lying type guards | Inferred predicates, tested guards | And the one config line that costs nothing to add: { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } } TypeScript gives you a checker that catches most mistakes and tells you in writing which ones it lets through. Add noUncheckedIndexedAccess today, and put the other six on your review checklist. --- If this was useful Every program in this post comes down to one question: what does assignability actually check? The TypeScript Type System is the book on that question. It covers variance and bivariance, including the function-versus-method split behind programs 1 and 2, plus the generic, conditional and branded types you reach for when the built-in rules are too loose. The TypeScript Library has five books. Start with the first two if TypeScript is your main language, swap in a bridge book if you are coming from the JVM or PHP, and add the fifth if you ship TypeScript at work: - TypeScript Essentials — types, narrowing, modules, async, daily-driver tooling - The TypeScript Type System — generics, mapped/conditional types, infer, template literals, branded types - Kotlin and Java to TypeScript — JVM developers moving to TypeScript - PHP to TypeScript — PHP 8+ developers moving to TypeScript - TypeScript in Production — tsconfig, build tools, monorepos, library authoring, dual ESM/CJS, JSR All five books ship in ebook, paperback, and hardcover. ![The TypeScript Library — the 5-book collection](xgabriel.com)

    1 reaction · 1 comment
  8. DEV · · @fabwaseem

    TypeScript 7 Upgrade Guide: What Breaks, ESLint, Next.js and Real Benchmarks I tested the TypeScript 7 upgrade and ran into three catches that are easy to miss: removed tsconfig options, a typescript-eslint break, and a side-by-side setup that changes which compiler next build uses. Here is the full guide with the exact errors and fixes. TypeScript 7 is the native, Go-based rewrite of the TypeScript compiler, and it has been the latest version on npm since July 8, 2026. It type-checks the same code as TypeScript 6 but much faster. On this box Hono's source built 5.2x faster (3.01 s to 0.58 s), with identical JavaScript output. The upgrade is usually one npm install, with three real catches. Every option deprecated in TypeScript 6 is now a hard error. TypeScript 7 has no programmatic API, so typescript-eslint refuses to run. And the side-by-side workaround quietly changes which compiler next build uses. I hit all three while testing, so this guide gives the exact errors and fixes, cited to the official TypeScript 7 announcement. What is TypeScript 7? TypeScript 7 is a faithful port of the TypeScript compiler and language service from TypeScript to Go. It ships as the normal typescript npm package with the same tsc command. Microsoft says it "typically" yields "speedups between 8x and 12x on full builds" through native code and shared-memory multithreading. The type system and emitted JavaScript match TypeScript 6.0. Key facts at a glance: | | TypeScript 6.0 | TypeScript 7.0 | | --- | --- | --- | | Written in | TypeScript (runs on Node.js) | Go (native binary, multithreaded) | | npm package | typescript@6 / @typescript/typescript6 | typescript@7 (latest, 7.0.2) | | Type checking | Reference behavior | Same semantics, with stableTypeOrdering always on | | Deprecated options | Error, silenced by ignoreDeprecations: "6.0" | Hard error, cannot be silenced | | Programmatic API | Yes (ts.createProgram, ...) | None yet; planned for 7.1 | | Editor | Built-in TS server | LSP server, VS Code extension | The announcement adds that "practically any TypeScript code that compiles cleanly with TypeScript 6.0 (with the stableTypeOrdering flag on, and without any ignoreDeprecations flag set) should compile identically in TypeScript 7.0." Is TypeScript 7 stable and ready for production? Yes, for compiling and type-checking. TypeScript 7.0 is a stable release, and the team tested it with companies including Bloomberg, Canva, Figma, Google, Linear, Notion, Sentry, Slack and Vercel. It is not ready for tools that need the compiler API, such as typescript-eslint and Vue, Svelte, Astro or MDX tooling, until TypeScript 7.1 ships a new API. Quotable results from the announcement: - Slack's CI type-checking went "from about 7.5 minutes to 1.25 minutes." - The new language server "reduced failing language server commands by over 80%, and reduced server crashes by over 60%" compared with 6.0. - Opening a file with an error in the VS Code codebase dropped from about 17.5 seconds to under 1.3 seconds. The TypeScript 7.1 iteration plan lists "Stabilize API" as its first item. It scheduled the 7.1 beta for October 6 and the stable release for November 24, 2026. On October 17, 2026 npm still had no 7.1 beta, only nightly 7.1.0-dev builds under the next tag, so treat the date as a target. How much faster is TypeScript 7? (my benchmark) On an 8-vCPU Linux box, TypeScript 7.0.2 built Hono's source (189 files, about 26,000 lines, declarations plus JavaScript) in a median 0.58 seconds, against 3.01 seconds for TypeScript 6.0.3. That is a 5.2x speedup with 27% less peak memory. Both compilers emitted byte-identical JavaScript for all 188 output files. The run: tsc -p tsconfig.build.json on Hono at commit 5ca49b0, three runs each, Node.js 24.22.0, measured with os.wait4 for wall time and peak RSS. | Compiler | Median | Runs | Peak memory | | --- | --- | --- | --- | | TypeScript 6.0.3 | 3.01 s | 2.85, 3.01, 3.08 | 528 MB | | TypeScript 7.0.2 (default, 4 checkers) | 0.58 s | 0.58, 0.50, 0.59 | 383 MB | | TypeScript 7.0.2 --checkers 8 | 0.61 s | 0.61, 0.60, 0.62 | 416 MB | | TypeScript 7.0.2 --singleThreaded | 0.98 s | 0.98, 1.03, 0.89 | 250 MB | !Bar chart of the TypeScript 6 vs 7 benchmark on Hono: TypeScript 6.0.3 3.01 seconds, TypeScript 7.0.2 0.58 seconds, --checkers 8 0.61 seconds, --singleThreaded 0.98 seconds, with peak memory for each My 5.2x is below Microsoft's 8x to 12x because this project is small. Startup time weighs more on a 3-second build than on VS Code's 125-second one, so expect larger gains on larger codebases. Two things surprised me: - More checkers didn't help here. --checkers 8 was no faster than the default of 4 and used more memory. The announcement says extra checkers help "larger codebases" at "the cost of increased memory usage." - Even single-threaded, TypeScript 7 was 3x faster and used half the memory. On small CI runners --singleThreaded or --checkers 1 is a reasonable choice. Does TypeScript 7 emit the same output? The JavaScript was identical. The .d.ts files were not: 25 of 188 declaration files differed. In 16 the only change was quote style ('/' instead of "/"). In 9, TypeScript 7 wrote a reference such as toArray: typeof toArray where TypeScript 6 had inlined the full function type: - toArray: (children: Child) => Child[]; + toArray: typeof toArray; These are equivalent types, but if you publish a library, diff your generated declarations or run your API report tool before releasing a version built with TypeScript 7. What breaks when upgrading to TypeScript 7? Three things break most often. Every compiler option deprecated in TypeScript 6.0 is now a hard error that ignoreDeprecations no longer silences. The new defaults (types: [], rootDir: "./", strict: true) surface errors in existing configs. And any tool that imports the typescript API fails, because 7.0 ships without one. Removed tsconfig options I ran a deliberately old tsconfig.json through both compilers. TypeScript 6 already reports these as errors (TS5101/TS5107, "will stop functioning in TypeScript 7.0"), but you can silence them with "ignoreDeprecations": "6.0". TypeScript 7 reports TS5102/TS5108 and ignores that flag (with it set, 7.0.2 still exited 1): tsconfig.json(3,15): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration. tsconfig.json(5,25): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration. tsconfig.json(6,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration. Use '"paths": {"*": ["./*"]}' instead. tsconfig.json(7,24): error TS5090: Non-relative paths are not allowed. Did you forget a leading './'? tsconfig.json(8,5): error TS5102: Option 'downlevelIteration' has been removed. Please remove it from your configuration. tsconfig.json(9,24): error TS5108: Option 'esModuleInterop=false' has been removed. Please remove it from your configuration. The full list of removals and their replacements, from the announcement: | Removed in TypeScript 7 | Use instead | | --- | --- | | target: "es5", downlevelIteration | es2015 or later (a bundler can down-level if you must) | | moduleResolution: "node" / "node10" / "classic" | "nodenext" (Node) or "bundler" (Vite, Next.js, webpack) | | module: "amd", "umd", "systemjs", "none" | "esnext" or "preserve" | | baseUrl | paths with ./-relative entries | | esModuleInterop: false, allowSyntheticDefaultImports: false | Remove (always on) | | alwaysStrict: false | Remove (always on) | | module Foo {} namespace syntax | namespace Foo {} | | import ... assert { type: "json" } | import ... with { type: "json" } | | tsc file.ts inside a folder with tsconfig.json | tsc --ignoreConfig file.ts | New defaults that cause errors After removing those options, two TypeScript 6 defaults (which 7 keeps) produced new errors: error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. src/index.ts(2,19): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig. rootDir now defaults to ./, and types now defaults to [], so @types/* packages are no longer included automatically. This is the config that compiled and ran on TypeScript 7.0.2: { "compilerOptions": { "target": "es2022", "module": "nodenext", "outDir": "dist", "rootDir": "./src", "types": ["node"] }, "include": ["src"] } Other defaults to know: strict is true, module is esnext, noUncheckedSideEffectImports is true, and stableTypeOrdering is always on. Language and JavaScript changes Template literal inference now keeps Unicode code points whole. This compiles on TypeScript 7 and fails on 6, which also makes it a quick way to prove which compiler a tool is running: // lib/which-ts.ts — compiles only on TypeScript 7 type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never export const check: HeadTail<'😀abc'> = ['😀', 'abc'] TypeScript 6 reports Type '"😀"' is not assignable to type '"�"', because it splits the emoji into two UTF-16 halves. JSDoc-typed JavaScript is also stricter: @enum, @class, a bare ? type and Closure-style function(string): void are no longer recognized. The CHANGES.md file tracks the full list. Does typescript-eslint work with TypeScript 7? No. typescript-eslint 8.71.2 (the latest on October 17, 2026) declares typescript >=4.8.4 <6.1.0 as a peer dependency and throws at startup when it finds TypeScript 7. Because eslint-config-next uses it, npm run lint fails in every Next.js app that upgrades. The fix is to keep TypeScript 6 installed under the typescript name for the API. This is the exact error from npx eslint . in a Next.js 16.4.2 app after npm i -D typescript@7.0.2: typescript-eslint does not support TS 7.0. Please see https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0 to run typescript-eslint using the TS 6 API. See also https://github.com/typescript-eslint/typescript-eslint/issues/10940 for tracking typescript-eslint's support for TS >=7.1 Oops! Something went wrong! :( ESLint: 9.39.5 Error: typescript-eslint does not support TS 7.0. Support is tracked in typescript-eslint issue #10940 and depends on the 7.1 API. How do I run TypeScript 7 and 6 side by side? Install TypeScript 7 under an alias such as @typescript/native, and install @typescript/typescript6 under the typescript name. Tools that import typescript (ESLint, Vue, Svelte) get the 6.0 API, while npx tsc runs the TypeScript 7 binary. This is the setup the TypeScript team recommends in its announcement. { "devDependencies": { "@typescript/native": "npm:typescript@^7.0.2", "typescript": "npm:@typescript/typescript6@^6.0.2" } } What I saw after npm install: $ npx tsc -v Version 7.0.2 $ npx tsc6 -v Version 6.0.3 $ npx eslint . (no output, exit 0) The compatibility package ships a tsc6 binary, so the two never fight over the name tsc. The catch: next build now uses TypeScript 6 With the alias setup, next build type-checks with TypeScript 6, not 7. Next.js resolves the package named typescript, finds the compatibility package and runs its tsc6. I proved it with the emoji file above: npx tsc passed, but next build failed with the TypeScript 6 error: Running TypeScript ... lib/which-ts.ts(3,42): error TS2322: Type '"😀"' is not assignable to type '"�"'. Failed to type check. So pick one of two setups until typescript-eslint supports 7.1: | Setup | next build checks with | ESLint (type-aware) | Best for | | --- | --- | --- | --- | | A. typescript@7 only | TypeScript 7 (fast) | Broken | Projects that lint with Biome or oxlint, or no type-aware lint | | B. Alias: typescript = TS 6, @typescript/native = TS 7 | TypeScript 6 | Works | Projects on eslint-config-next | With setup B, add npx tsc --noEmit as its own CI step to get TypeScript 7's speed for type checking. Next.js's typescript.ignoreBuildErrors would skip the slower check in next build, but only use it if that tsc step blocks the merge, because the Next.js docs call that option "very dangerous" on its own. !Decision flow for upgrading to TypeScript 7: fix TypeScript 6 deprecations first, then if the project uses typescript-eslint, Vue, Svelte, Astro or MDX tooling, use the alias setup with tsc 7 in CI, otherwise install typescript@7 directly Does Next.js support TypeScript 7? Yes. Since Next.js 16.3, next build can type-check with TypeScript 7, and in 16.4 it uses the project's tsc CLI by default (experimental.useTypeScriptCli: true), which works without the compiler API. Install typescript@^7 and it is used automatically. In my test the "Running TypeScript" step dropped from 1,368 ms to 231 ms. The Next.js 16.3 release post says: "To start using TypeScript 7 for type checking during next build, just bump your project's local dependency." The TypeScript config docs add the trade-off: CLI type checking "prints the native tsc diagnostics" and "does not apply Next.js-specific code frames," and it checks every file your tsconfig.json includes, test files too. My measurements on the Next.js 16.4.2 app from my use cache guide: | | TypeScript 5.9.3 (create-next-app default) | TypeScript 7.0.2 | | --- | --- | --- | | npx tsc --noEmit | 1.37 s | 0.38 s | | next build "Finished TypeScript in" | 1,368 ms | 231 ms | | Real type error caught by next build | Yes | Yes (TS2322, build fails) | create-next-app 16.4.2 still writes "typescript": "^5" into new projects, so new apps start on 5.9 until you upgrade. The app's default tsconfig (moduleResolution: "bundler", no baseUrl) needed no changes for TypeScript 7. TypeScript 7 upgrade checklist To upgrade safely, get to TypeScript 6.0 with zero deprecation errors first, then install TypeScript 7, fix the types and rootDir defaults, decide how to keep the 6.0 API for ESLint and framework tooling, and add TypeScript 7 to CI. Library authors should also diff their generated .d.ts files before publishing. 1. Upgrade to TypeScript 6.0 and remove ignoreDeprecations. Fix every TS5101/TS5107 deprecation error; each becomes a hard TypeScript 7 error. 2. Replace baseUrl with ./-relative paths, and moduleResolution: "node" with "bundler" or "nodenext". 3. Install TypeScript 7: npm i -D typescript@^7 (or the alias setup if step 6 applies). 4. Add "types": ["node"] (plus jest, bun and so on) if globals like process stop resolving. 5. Set rootDir when tsconfig.json sits above src and you emit files. 6. Check API-dependent tools: typescript-eslint, eslint-config-next, Vue/Volar, Svelte, Astro, MDX, Angular templates. If you use any, keep typescript pointed at @typescript/typescript6. 7. Add npx tsc --noEmit to CI and check that it runs version 7 (npx tsc -v). 8. Tune parallelism: try --checkers 1 or --singleThreaded on small CI runners and higher --checkers on large monorepos, and measure. 9. Libraries: diff .d.ts output between 6 and 7 before publishing. 10. Editor: install the TypeScript 7 VS Code extension, and use "Disable TypeScript 7 Language Server" for Vue or Svelte projects. Common TypeScript 7 upgrade mistakes The most common mistakes are upgrading straight from 5.x without passing through 6.0's deprecation errors, assuming the alias setup makes next build use TypeScript 7, forgetting the new types: [] default, and trusting a passing build while lint is silently skipped. Each produces either a confusing error or a slower build than you expected. | Mistake | Symptom | Fix | | --- | --- | --- | | Skipping TypeScript 6 | Wall of TS5102/TS5108 errors | Upgrade to 6.0 first and clear its deprecations | | Keeping ignoreDeprecations: "6.0" | Still errors on 7 | Remove it and fix the options | | typescript@7 with eslint-config-next | "typescript-eslint does not support TS 7.0" | Alias setup B | | Alias setup but expecting fast next build | Build still uses tsc6 | Run npx tsc --noEmit in CI as well | | Relying on automatic @types/* | Cannot find name 'process' (TS2591) | "types": ["node"] | | Upgrading a Vue, Svelte or Astro app's editor | Lost template type checking | Keep TypeScript 6 for the editor | | Publishing a library without checking .d.ts | Different declaration text | Diff and review before release | If you deploy through Docker, rebuild without cache after changing the compiler. My Next.js Dockerfile guide shows a multi-stage build where npm run build, and with it the type check, runs in the builder stage. Related: Prisma 8 needs TypeScript 5.9 or newer, and its client type-checked cleanly on TypeScript 7.0.2 in my test. See Prisma 8 vs Prisma 7: should you upgrade? Related: on NestJS, TypeScript 7 isn't usable through the Nest CLI yet: nest build asks for TypeScript 6 until 7.1. Plain tsc 7 works. Details in my NestJS 12 migration guide. Related: Oxlint's type-aware mode runs on typescript-go too. On this site's code it matched all 80 typescript-eslint findings in 1.29 s vs 20.5 s; details in Oxlint vs Biome vs ESLint. Key takeaways - TypeScript 7 is the native Go port of TypeScript, latest on npm since July 8, 2026 (7.0.2), with the same type system as 6.0. - On Hono's source it built 5.2x faster than TypeScript 6 on my 8-vCPU box, with identical JavaScript output. - Everything deprecated in TypeScript 6 is a hard error in 7: es5, node10 resolution, baseUrl, AMD/UMD modules, esModuleInterop false. - New defaults bite: types is [] and rootDir is ./, so add "types": ["node"] and an explicit rootDir. - typescript-eslint does not support TypeScript 7; keep @typescript/typescript6 under the typescript name until 7.1 ships an API. - Next.js 16.3+ type-checks with TypeScript 7 automatically, but the alias setup makes next build use TypeScript 6, so run npx tsc in CI too. - Library authors should diff .d.ts output, which can differ in quote style and typeof references. --- Originally published on fabwaseem.com.

  9. Reddit · · r/ClaudeAI · u/loonpwn

    I built a CLI and Skill to help Claude Code refactor faster with fewer tokens An IDE already knows how to rename a symbol and update its references. I wanted Claude Code to use those operations through its shell. So I built RipIDE, a free, open-source CLI and Skill for symbol renames, moving exports, and updating imports. I used Claude Code to help replace ts-morph with TypeScript’s native language server. That meant connecting four tools: - ripgrep to find candidate files - TypeScript’s native server for references and rename edits - Oxc for parsing and syntax transforms - Volar for Vue scripts and templates One trap during that work: the server could rename a declaration but preserve its old re-export name. Consumers kept using the old name. The rename preferences had to go through workspace/configuration before requests, so they actually took effect. That migration and the configuration detail are recorded in this commit. Getting the tools to agree was harder than I expected. Commands preview changes by default. Supported refactors check for new type errors before applying. harlan-zw/ripide Any refactoring cases that you think would be useful :)

    1 point · 2 comments
  10. I suppose my comment sort of implies this. I think the naive knee jerk reaction is that if you can spit out perfect machine level code against a specification, then logically being closer to bare metal would help with performance. However, without knowing the details of how things worked it is tough to say whether it is truly "reasonable" or not. But more importantly, the Typescript team themselves [1] picked Golang mostly out of ergonomics and ease of porting at the time. It is becoming more increasingly more to "taste" what ergonomics means. Notably, they did not pick it because they believed it to be the fastest option. But speed was on the mind, giving way to ergonomics and ease of porting. At least, this is my read. [1] typescript-go#411

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

1
PackageVersionDownloads · 30dTrend · 30d
v9.0.3
≥31.1M

README badges

Embed a badge in your README or website.

  • microsoft/typescript-go reached #1 in daily stars on gitwal · Mar 11, 2025
  • microsoft/typescript-go reached #1 in weekly stars on gitwal · Week of Mar 10, 2025
Live mentions · last 30 days
microsoft/typescript-go mentions in the last 30 days