R I B U T I V E なぜ重い? distribute.ts // Handler<E> = 自前のユーティリティ型(E から payload / handler を引く、など) E = "click" | "hover" | "drag" | ... (N 個) E extends string ? Handler<E> : never ↓ TS が要素ごとに展開 (distributive conditional) Handler<"click"> | Handler<"hover"> | Handler<"drag"> | ... ↑ N 個の独立した instantiation CROSS-PRODUCT N × c サイズ N 個分の 独立した計算 @dOwOd · TSKaigi 2026 LT 12 / 28 § 2 Union のサイズ N × Handler 1 回の計算量 c = N × c N=1000, c=150 → 150,000 instantiation 「Union を渡したら、中で勝手にバラバラにされる」という TS の振る舞い
R E / A F T E R 計測 サイズ BEFORE AFTER N=20 3,000 inst 40 inst N=500 75,000 inst 1,000 inst N=1000 150,000 inst 2,000 inst N=1000 比 75× の instantiation 差 @dOwOd · TSKaigi 2026 LT 13 / 28 § 2
E D よくある業務例 paths.ts // zod schema / GraphQL Codegen / form path type FormPaths<T> = { [K in keyof T as `${string & K}.${number}`]: T[K] }; type UserPaths = FormPaths<User>; // ← as 句で全展開 @dOwOd · TSKaigi 2026 LT 15 / 28 § 3 as 句が入ると homomorphic 性が崩れる
A N T I A T I O N S 計測 K (KEY 数) HOMOMORPHIC ( AS 句なし) AS 句あり K=20 9 inst 96 inst K=200 9 inst 816 inst K=1,000 9 inst 4,016 inst K=10,000 9 inst 40,016 inst → homomorphic は K に依存せず 9 inst で一定 / as 句あり は K に線形比例 (K=10,000で約 4,400× の差) ※ tsc 6.0 + --extendedDiagnostics 実測。 homomorphic = { [K in keyof T]: T[K] } 、as 句あり = { [K in keyof T as Rename<K>]: T[K] } @dOwOd · TSKaigi 2026 LT 17 / 28 § 3
T & F R E E Z E 解決策: 「分けて固める」 split-keys.ts // Before: as 句で動的にキーを生成 type Paths<T> = { [K in keyof T as Rename<K>]: T[K] }; // After: 一旦 key だけ計算して固定、value は homomorphic を維持 type Keys<T> = { [K in keyof T]: Rename<K> }[keyof T]; type Paths<T> = { [K in Keys<T>]: ValueFor<T, K> }; // ^ 普通の mapped 、homomorphic キーを 先に確定してから、値を埋める @dOwOd · TSKaigi 2026 LT 18 / 28 § 3