Build & Emit
check กับ emit เป็นงานของเครื่องมือคนละตัว
หัวข้อที่มีชื่อว่า “check กับ emit เป็นงานของเครื่องมือคนละตัว”โมดูลนี้เปิดด้วยไอเดียว่า tsc ทำสองงาน — check กับ emit ใน setup สมัยใหม่งานสองอย่างนี้มักถูกทำโดย เครื่องมือคนละตัว และการเข้าใจว่าทำไมทำให้ config ชัดเจนขึ้น
flowchart LR src["source .ts"] --> bundler["bundler (esbuild/swc/vite) strips types, emits JS fast"] src --> tsc["tsc --noEmit checks types"] bundler --> out["shipped JavaScript"] tsc --> ci["pass/fail in CI"]
- bundler (esbuild, swc, Vite) transpile ไฟล์แต่ละไฟล์ โดย strip type ออก เร็วมากเพราะไม่เคย check อะไรเลย — แค่ดูทีละไฟล์แล้วลบ type ทิ้ง
tsc --noEmitทำ type check ทั้งโปรแกรมและไม่ produce output ออกมา นี่คือตัวที่จับ bug ของคุณจริง ๆ และคุณควรรันใน CI (และ editor ก็รันให้คุณตลอดเวลา)
ship สิ่งที่ bundler emit; เชื่อสิ่งที่ tsc รายงาน noEmit บอก tsc ให้เป็น checker ล้วน ๆ
isolatedModules: ทำให้ transpile ทีละไฟล์ปลอดภัย
หัวข้อที่มีชื่อว่า “isolatedModules: ทำให้ transpile ทีละไฟล์ปลอดภัย”เพราะ bundler transpile ทีละไฟล์ จึงมองไม่เห็น type จากไฟล์อื่น มี TypeScript construct ไม่กี่อย่างที่กำกวมภายใต้ single-file transpilation — ที่สำคัญที่สุดคือ การ re-export type ดูเหมือนกับการ re-export value เป๊ะ isolatedModules: true ทำให้ tsc flag อะไรก็ตามที่ single-file transpiler อาจทำพลาด เพื่อให้ code ของคุณเข้ากันได้กับ esbuild และ swc
// with isolatedModules, tsc forces you to be explicit here:export type { User } from "./user"; // ✅ clearly a type re-exportexport { User } from "./user"; // ❌ ambiguous: is User a type or a value?type-only import
หัวข้อที่มีชื่อว่า “type-only import”การแก้ข้างบนเป็นส่วนหนึ่งของ feature ที่กว้างกว่า: type-only import และ export การ mark import เป็น type-only บอก compiler ว่ามีอยู่เพื่อ type information ล้วน ๆ และต้องถูก erase — ไม่ emit เป็น runtime import เด็ดขาด
import type { User } from "./user"; // erased entirely at emitimport { type User, save } from "./db"; // User erased, save keptverbatimModuleSyntax: true บังคับวินัยนี้: import ตัวไหนที่ ไม่ ถูก mark type จะถูก emit ตรง ๆ เป็น runtime import setting นี้กำจัด class ของเรื่องเซอร์ไพรส์ที่ compiler แอบ drop หรือเก็บ import ไว้ และเป็น setting ที่แนะนำสำหรับโปรเจกต์ใหม่
project references สำหรับ monorepo
หัวข้อที่มีชื่อว่า “project references สำหรับ monorepo”ใน repo ใหญ่ที่มีหลาย package การ type-check ทุกอย่างใหม่หมดทุกครั้งที่แก้จะช้า project references ให้คุณแบ่ง codebase เป็นโปรเจกต์ที่ build แยกกันได้พร้อม dependency ที่ประกาศไว้:
{ "references": [{ "path": "../core" }], "compilerOptions": { "composite": true }}เมื่อเปิด composite: true tsc --build จะ check แต่ละโปรเจกต์ที่ reference ครั้งเดียว, cache ผลไว้ และ recheck เฉพาะสิ่งที่เปลี่ยน วิธีนี้เปลี่ยนการ check แบบ O(ทั้ง repo) ให้เป็นแบบ incremental — วิธีมาตรฐานในการทำให้ type-checking เร็วเมื่อ monorepo โตขึ้น