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/Rolldown) transpile ไฟล์แต่ละไฟล์ โดย strip type ออก เร็วเพราะไม่เคย check อะไรเลย — แค่ดูทีละไฟล์แล้วลบ type ทิ้ง
tsc --noEmitทำ type check ทั้งโปรแกรมและไม่ produce output ออกมา นี่คือตัวที่จับ bug ของคุณจริง ๆ และคุณควรรันใน CI (และ editor ก็รันให้คุณตลอดเวลา)
ship สิ่งที่ bundler emit; เชื่อสิ่งที่ tsc รายงาน noEmit บอก tsc ให้เป็น checker ล้วน ๆ เมื่อก่อนการแยกแบบนี้มีเหตุผลเรื่องความเร็ว — “tsc ช้าเกินกว่าจะอยู่ใน hot path” — และข้ออ้างนั้นแทบระเหยไปแล้วอย่างที่ section ถัดไปอธิบาย แต่การแยกยังคงอยู่ด้วยเหตุผลที่ดีกว่า: bundler ทำสิ่งที่ tsc ไม่มีวันทำ (tree-shaking, code-splitting, จัดการ asset) และ checker ไม่ควรต้องสนใจว่าคุณ bundle อย่างไร
native compiler (TypeScript 7)
หัวข้อที่มีชื่อว่า “native compiler (TypeScript 7)”TypeScript 7.0 (กรกฎาคม 2026) คือ compiler และ language server ที่ เขียนใหม่ด้วย Go จากฝั่งคุณแทบไม่มีอะไรเปลี่ยน: npm install -D typescript ติดตั้งมันให้, binary ยังชื่อ tsc, tsconfig.json ยังเป็นไฟล์เดิม และรายงาน error ชุดเดิม สิ่งที่เปลี่ยนคือนาฬิกา:
| Codebase | TypeScript 6 | TypeScript 7 (--checkers 4) | เร็วขึ้น |
|---|---|---|---|
| VS Code | 125.7 s | 10.6 s | 11.9× |
| Sentry | 139.8 s | 15.7 s | 8.9× |
| Playwright | 12.8 s | 1.47 s | 8.7× |
| tldraw | 11.2 s | 1.46 s | 7.7× |
memory ลดลง 6–26% พร้อมกัน และ editor รายงาน error แรกใน repo ของ VS Code ที่ราว 1.3 s แทนที่จะเป็น 17.5 s ความเร็วมาจากไหน และมันหมายถึงอะไรสำหรับคุณ:
- type-check แบบขนาน checker แบ่งโปรแกรมไปหลาย worker thread:
tsc --checkers 4คือ default,--checkers 8ดัน VS Code ไปถึง 16.7× และ--singleThreadedปิดมันเมื่อคุณอยากได้ profiling แบบ deterministic ใน--buildmode--builders Ncheck project reference ที่เป็นอิสระต่อกันแบบขนาน --watchที่สร้างใหม่ การ watch ไฟล์ตอนนี้ใช้ native watcher (Parcel watcher ที่ port มาเป็น Go) แทนการ poll ดังนั้น watch mode บน repo ใหญ่ไม่กิน CPU core ค้างไว้อีกต่อไป- การเรียงลำดับ type แบบ deterministic type ใน error message และ output
.d.tsถูกพิมพ์ด้วยลำดับที่คงที่ ไม่ว่า thread ไหนจะเจอมันก่อน TypeScript 6.0 เปิดเรื่องนี้ให้ใช้ผ่าน--stableTypeOrderingเพื่อให้คุณ diff output ของ compiler ทั้งสองก่อนสลับ - ไม่มี programmatic API ใน 7.0
import ts from "typescript"สำหรับ custom transformer, linter และ plugin ของ language tool ยังไม่มี — 7.1 จะ ship API ตัวใหม่ ระหว่างนี้เครื่องมือที่ต้องใช้ API (typescript-eslint, Volar) รัน TypeScript 6 ผ่าน package@typescript/typescript6และคุณติดตั้งทั้งสองตัวคู่กันได้:
{ "devDependencies": { "typescript": "^7.0.2", "@typescript/typescript6": "^6.0.2" }}- editor VS Code ship language server ของ TypeScript 7 มาให้ (มีคำสั่ง “Disable TypeScript 7 Language Server” ให้ถอยกลับไป 6 ถ้าจำเป็น); อะไรที่เป็น LSP ใช้กับมันได้หมด framework ที่ template checking ฝัง compiler ไว้ — Vue, Svelte, Astro, Angular — ยังใช้ 6.0 ใน editor จนกว่า API ของ 7.1 จะมา ดังนั้น setup แบบ hybrid (7 สำหรับ
tsc, 6 สำหรับ editor) เป็นเรื่องปกติในโปรเจกต์พวกนั้นตอนนี้
มีการเปลี่ยนพฤติกรรมสองเรื่องที่ควรรู้: JavaScript ที่ใช้แค่ JSDoc เสีย form แบบ Closure ไป (@enum, type ? เดี่ยว ๆ, postfix !, @class) และ infer ใน template literal ตอนนี้แยก string ตาม Unicode code point ดังนั้น HeadTail<"😀abc"> เป็น ["😀", "abc"] ไม่ใช่ surrogate ครึ่งซีกสองตัว
รัน TypeScript โดยไม่ต้อง build
หัวข้อที่มีชื่อว่า “รัน TypeScript โดยไม่ต้อง build”Node 22.6+ (เป็น default ตั้งแต่ 23.6 และในทุก Node 24 LTS) รัน node app.ts ได้ตรง ๆ ด้วยการ strip type annotation ออก — ไม่ต้องมี tsc, bundler หรือ loader Deno กับ Bun ทำแบบนี้มาหลายปีแล้ว จุดที่ต้องระวังคือคำว่า strip: runtime ลบ type ทิ้ง ไม่ได้ transform syntax อะไรก็ตามที่ TypeScript เพิ่มเข้ามาแล้วต้อง emit JavaScript จริง — enum, namespace ที่มี code ข้างใน, parameter property ของ class, import x = require() — จะถูกปฏิเสธ
erasableSyntaxOnly: true (5.8) ทำให้ tsc บังคับกฎนั้นตอน check เลย เพื่อให้ code ที่คุณ type-check คือ code ที่ Node รันได้:
// with erasableSyntaxOnlyclass Point { constructor(public x: number, public y: number) {} // ❌ parameter properties need emit (TS1294)}enum Color { Red, Green } // ❌ enums need emit
class Point2 { readonly x: number; // ✅ an ordinary field assignment instead — constructor(x: number) { this.x = x; } // readonly on a parameter property still needs emit too}const Color2 = { Red: "red", Green: "green" } as const; // ✅ a const object insteadmodifier ไหนก็ตามที่ใส่บน parameter — public, private, readonly — ทำให้มันเป็น parameter property และ parameter property ต้อง emit this.x = x ให้เสมอ ไม่มีตัวไหน erasable เลย constructor(readonly x: number) {} ยังพัง TS1294 อยู่ดีภายใต้ erasableSyntaxOnly และ Node --experimental-strip-types ก็ปฏิเสธด้วย (ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX: TypeScript parameter property is not supported in strip-only mode) — ทางแก้คือเขียน field กับการ assign เองเสมอ อย่างที่ Point2 ทำข้างบน
จับคู่กับ rewriteRelativeImportExtensions แล้วคุณ develop กับไฟล์ .ts ตรง ๆ ได้ และยัง emit package .js สะอาด ๆ ด้วย tsc เพื่อ publish
ไฟล์เดียว สี่เครื่องมือ: แต่ละตัวทำอะไรกับ syntax ที่ไม่ erasable จริง ๆ
หัวข้อที่มีชื่อว่า “ไฟล์เดียว สี่เครื่องมือ: แต่ละตัวทำอะไรกับ syntax ที่ไม่ erasable จริง ๆ”“compile ผ่าน” ซ่อนความต่างจริงระหว่างเครื่องมือ การรันไฟล์เดียวกันเป๊ะ — enum กับ class ที่มี parameter property — ผ่าน tsc, tsx, node --experimental-strip-types, และ esbuild (ตรวจใน scratch แล้ว) ทำให้ความต่างเห็นชัดเจน:
enum Color { Red, Green }
class Point { constructor(public x: number, public y: number) {}}
const p = new Point(1, 2);console.log(Color.Red, p.x, p.y);| เครื่องมือ | ทำอะไร | ผลลัพธ์ |
|---|---|---|
tsc --noEmit --strict | type-check เท่านั้น | 0 error — enum กับ parameter property เป็น TypeScript ที่ถูกต้องสมบูรณ์ |
tsc (emit จริง) | transpile โดยเข้าใจ syntax เต็มรูปแบบ | emit IIFE จริงสำหรับ enum และ this.x = x; this.y = y; จริงสำหรับ parameter property — รันได้ปกติ |
tsx | transpile ผ่าน esbuild แล้วรันทันที | รันได้ปกติ — พิมพ์ 0 1 2 |
node --experimental-strip-types | strip type annotation อย่างเดียว ไม่ transform อะไรเลย | SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode |
esbuild (transpile แบบ bundler) | transpile โดยเข้าใจ syntax เหมือน tsc | emit IIFE ที่เทียบเท่ากัน รันได้ปกติ |
สี่เครื่องมือ สามผลลัพธ์ต่างกันจาก input เดียวกันเป๊ะ และ type checker เพียว ๆ แยกไม่ออกเลยสักตัว — tsc --noEmit บอก “ปกติดี” ไม่ว่าอีกสามตัวไหนจะรัน result นี้ได้จริงหรือเปล่า นี่คือสิ่งที่ erasableSyntaxOnly ปิดช่องว่าง: มันทำให้ checker เองปฏิเสธอะไรก็ตามที่มีแค่ tsc/tsx/esbuild แบบ transformation (ไม่ใช่การ strip เพียว ๆ) เท่านั้นที่จัดการได้ ดังนั้น check ที่ผ่านภายใต้ flag นั้นจึงทำนายได้จริงว่า stripper ของ Node จะยอมรับไฟล์นั้น
decorator แยกสามเครื่องมือที่เหลือออกจากกันอีก class เดียวกันที่มี method decorator @logged ถูก transpile โดย esbuild ที่ setting default (target ประมาณ esnext) แล้วรันตรง ๆ ด้วย node เฉย ๆ พังด้วย SyntaxError: Invalid or unexpected token — decorator syntax ถูกส่งผ่านไปเฉย ๆ โดยไม่แตะ และ V8 ยัง parse มันตามธรรมชาติไม่ได้ tsx รัน source เดียวกันเป๊ะได้สำเร็จ เพราะมันตั้ง --target ของ esbuild ให้ต่ำพอที่จะบังคับให้ lower decorator แทนที่จะปล่อยผ่าน (ตรวจใน scratch แล้ว: esbuild --target=es2022 บนไฟล์เดียวกันผลิต helper การ lowering จริง แม้จะยาว แล้วรันถูกต้อง) บทเรียนไม่ใช่ “esbuild พัง” — แต่คือ “transpile ด้วย esbuild” ไม่ใช่พฤติกรรมคงที่ตัวเดียว มันขึ้นกับ target ที่ผู้เรียกตั้งไว้ และ default ของ tsx กับการเรียก esbuild เปล่า ๆ ไม่ใช่เครื่องมือเดียวกันในทางปฏิบัติ
isolatedModules: ทำให้ transpile ทีละไฟล์ปลอดภัย
หัวข้อที่มีชื่อว่า “isolatedModules: ทำให้ transpile ทีละไฟล์ปลอดภัย”เพราะ bundler และ type-stripper ทำงาน ทีละไฟล์ จึงมองไม่เห็น type จากไฟล์อื่น มี TypeScript construct ไม่กี่อย่างที่กำกวมภายใต้ single-file transpilation — ที่สำคัญที่สุดคือ การ re-export type ดูเหมือนกับการ re-export value เป๊ะ isolatedModules: true ทำให้ tsc flag อะไรก็ตามที่ single-file transpiler อาจทำพลาด เพื่อให้ code ของคุณเข้ากันได้กับ esbuild, swc และ stripper ของ Node
// @skip-verify illustrative — "./user" is not a real module// 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 เด็ดขาด
// @skip-verify illustrative — "./user" and "./db" are not real modulesimport 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 ไว้ และ tsc --init เปิดให้คุณอยู่แล้ว
แยก emit ออกจาก check อีกทาง: noCheck และ isolatedDeclarations
หัวข้อที่มีชื่อว่า “แยก emit ออกจาก check อีกทาง: noCheck และ isolatedDeclarations”noEmit คือ “check แต่ไม่ emit” ตั้งแต่ 5.6 มีภาพสะท้อนของมัน: noCheck คือ “emit แต่ไม่ check” — tsc รันเฉพาะ pass ระดับ syntax ที่ต้องใช้ผลิต output ตัวมันเองเป็นแค่ esbuild ที่ช้ากว่า แต่พอรวมกับ isolatedDeclarations (5.5) มันมีประโยชน์ขึ้นมา: ถ้าทุก export มี type ระบุชัด ไฟล์ .d.ts จะถูก generate ทีละไฟล์ได้โดยไม่ต้องใช้ checker ซึ่งแปลว่า declaration emit รันขนานกับ type-checking ได้ หรือให้ bundler plugin ทำทั้งหมดก็ได้
// @skip-verify needs --isolatedDeclarations, which this course's default check flags do not enable -- see prose for the real command and codeexport function parse(input: string) { return JSON.parse(input);}compile ด้วย --isolatedDeclarations --declaration --noEmit เพิ่มเข้าไปแล้ว จะได้ error TS9013: Expression type can't be inferred with --isolatedDeclarations (ตรวจใน scratch แล้ว — ไม่ใช่ TS9007 ที่ดูเหมือนจะใช่แต่ผิดสำหรับกรณีนี้เป๊ะ — ตรวจ code จริงเสมอ อย่าเชื่อจากความจำ) การเพิ่ม return type ชัดเจนตามที่บทก่อนหน้าของคอร์สนี้แนะนำสำหรับ public API แก้ได้โดยไม่ต้องเปลี่ยนอะไรอื่นเลย:
export function parse(input: string): unknown { return JSON.parse(input);}ราคาคือต้อง annotate public API ของคุณ — ซึ่งสำหรับ library คุณควรทำอยู่แล้ว
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 ผลไว้ใน .tsbuildinfo (เขียนเสมอตั้งแต่ 5.6) และ recheck เฉพาะสิ่งที่เปลี่ยน ตั้งแต่ 5.6 การ build เดินต่อแม้เจอ error ในโปรเจกต์ upstream เพื่อให้คุณเห็นทุก failure ในการรันเดียว — ใส่ --stopBuildOnErrors ถ้าอยากได้พฤติกรรม fail-fast แบบเดิม — และ TypeScript 7 check โปรเจกต์ที่เป็นอิสระต่อกันพร้อมกันด้วย --builders วิธีนี้เปลี่ยนการ check แบบ O(ทั้ง repo) ให้เป็นแบบ incremental และขนาน
การ build สองโปรเจกต์จริง (app ที่ reference core ตรวจใน scratch แล้ว) โชว์ทั้งการ cache และการ propagate error ผ่านสามครั้งของ tsc -b app --verbose เดียวกันเป๊ะ:
$ tsc -b app --verbose # 1) build สะอาด ยังไม่มี cacheProject 'core/tsconfig.json' is out of date because output file 'core/tsconfig.tsbuildinfo' does not existBuilding project 'core/tsconfig.json'...Project 'app/tsconfig.json' is out of date because output file 'app/tsconfig.tsbuildinfo' does not existBuilding project 'app/tsconfig.json'...
$ tsc -b app --verbose # 2) รันซ้ำทันที ไม่มีอะไรเปลี่ยนProject 'core/tsconfig.json' is up to date because newest input 'core/src/index.ts' is older than output 'core/tsconfig.tsbuildinfo'Project 'app/tsconfig.json' is up to date because newest input 'app/src/index.ts' is older than output 'app/dist/index.js'
$ tsc -b app --verbose # 3) หลังใส่ type error จริงใน core/src/index.tsProject 'core/tsconfig.json' is out of date because output 'core/tsconfig.tsbuildinfo' is older than input 'core/src/index.ts'Building project 'core/tsconfig.json'...core/src/index.ts(2,3): error TS2322: Type 'number' is not assignable to type 'string'.Project 'app/tsconfig.json' is out of date because output 'app/dist/index.js' is older than input 'core'Building project 'app/tsconfig.json'...การรันที่ 2 ไม่ rebuild อะไรเลย — ทุกโปรเจกต์รายงาน “up to date” จาก timestamp ของ .tsbuildinfo เพียว ๆ การรันที่ 3 คือพฤติกรรม default “เดินต่อแม้เจอ error” ตัวจริง: core พัง แต่ tsc -b ยังพยายาม build app ต่อไป (ใช้ output ล่าสุดที่ดีของ core) แทนที่จะหยุด — นั่นคือสิ่งที่ทำให้ทุก failure downstream โผล่มาในรอบเดียวบน monorepo จริง แทนที่จะทำทีละโปรเจกต์คนละรอบ การใส่ --stopBuildOnErrors เข้าไปใน state ที่พังเดียวกันนั้นเปลี่ยนผลลัพธ์: core ยังพังเหมือนเดิมเป๊ะ แต่ app ถูกรายงานชัดเจนว่า ถูกข้าม (“app/tsconfig.json can’t be built because its dependency ‘core’ has errors”) แทนที่จะพยายาม build — พฤติกรรม fail-fast แบบเดิม มีประโยชน์เมื่อการพยายาม build downstream กับ output ที่รู้อยู่แล้วว่าพัง จะแค่เป็น noise เฉย ๆ
strict เพียว ๆ: checker ปฏิเสธอันนี้เองไหม?
enum Color { Red, Green }
class Point { constructor(public x: number, public y: number) {}}
const p = new Point(1, 2);console.log(Color.Red, p.x, p.y); กด Check types — strict (เปิดอยู่เสมอที่นี่) รายงาน 0 error enum กับ parameter property เป็น TypeScript ที่ถูกต้องสมบูรณ์ตามปกติ ไม่มีอะไรใน strict สนใจเลยว่า runtime จะรัน result ได้โดยไม่ต้องแก้ไหม นี่คือช่องว่างเป๊ะที่ erasableSyntaxOnly ปิด — หลักฐานที่ตรวจด้วย CLI ของคอร์สนี้เองข้างบนแสดงว่าไฟล์เดียวกันเป๊ะถูกปฏิเสธตรง ๆ (TS1294) ทันทีที่ flag นั้นเปิด และถูกปฏิเสธอีกครั้งโดย node --experimental-strip-types ไม่ว่าจะมี flag อะไรเลย check ที่ผ่านสะอาดใน widget นี้ พิสูจน์แค่ว่า syntax ถูกต้องตามกฎหมาย มันไม่ได้บอกอะไรเลยว่าไฟล์ที่คุณกำลังจะรันด้วย runtime แบบ type-stripping จะรันได้จริงไหม
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกเครื่องมือในบทนี้อยู่บนแกนเดียวกัน: มันเข้าใจ TypeScript syntax ดีพอที่จะ transform มันไหม หรือรู้แค่วิธี ลบ type annotation ทิ้ง? tsc, tsx, และ esbuild (ที่ตั้งค่าถูกต้อง) คือ transformer — มันเปลี่ยน enum ให้เป็น object จริง, parameter property ให้เป็น assignment จริง, decorator ให้เป็น helper call จริงได้ --experimental-strip-types ของ Node คือ deleter — มันลบเฉพาะ token ที่เป็น syntax ของ type และปฏิเสธที่จะแตะอะไรอื่นเลย ซึ่งเป็นเหตุผลเป๊ะที่ enum กับ parameter property (construct ที่มีอยู่จริงตอน runtime แต่สวม syntax ของ TypeScript) ถูกปฏิเสธแทนที่จะถูก transform erasableSyntaxOnly คือ checker ยืมกฎของ deleter มาใช้เอง เพื่อให้ “ผ่าน check” กับ “stripper จะยอมรับไฟล์นี้” กลายเป็นข้อเรียกร้องเดียวกัน แทนที่จะเป็นสองเรื่องที่ไม่เกี่ยวกัน project references ใช้คำถาม checked/not-checked แบบเดียวกันในระดับ build graph ทั้งหมด: “up to date” ของ tsc -b คือข้อเรียกร้องจริงว่ายังต้อง check อะไรอีก หนุนหลังด้วย .tsbuildinfo ไม่ใช่การเดา
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| แนวทาง | ข้อดี | ต้นทุน |
|---|---|---|
emit ด้วย tsc | transform ทุก construct ถูกต้อง รวมถึง decorator กับ enum | ช้ากว่า single-file transpiler บน codebase ใหญ่ถ้าไม่มี --checkers/project references |
tsx / esbuild (ตั้งค่าให้รองรับ) | transpile เร็ว ทีละไฟล์ lower decorator/enum ได้จริงถ้า target ต่ำพอ | target default ของ bundler อาจไม่ lower ทุกอย่าง — ตรวจแล้วข้างบน esbuild เปล่า ๆ ทิ้ง decorator ที่พังตอน runtime ไว้ จนใส่ --target=es2022 ชัดเจน |
node --experimental-strip-types | ไม่มี build step เลยสำหรับ code ที่เข้ากันได้ | ปฏิเสธ TypeScript ที่ถูกต้องและใช้งานได้ทั้ง category (enum, parameter property, namespace ที่มี code) ตรง ๆ |
erasableSyntaxOnly | ทำให้ “type-check ผ่าน” กับ “stripper จะรันได้” เป็นการันตีเดียวกัน | บังคับให้เขียน construct ใหม่ที่จริง ๆ เป็น TypeScript ที่ดีอยู่แล้ว เพียงเพื่อความเข้ากันได้กับ stripper |
tsc --build + project references | check แบบ incremental และขนานจริงข้าม monorepo error โผล่ในรอบเดียวโดย default | ต้องใส่ --stopBuildOnErrors ชัดเจนเพื่อคืน fail-fast; ถ้าไม่ใส่ error ของ upstream ที่พังกับโปรเจกต์ downstream ที่ถูกข้ามจะโผล่มาด้วยกัน ต้องอ่านให้ละเอียด |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “type-check ผ่าน” แปลว่าเครื่องมือไหนก็รัน JavaScript ที่ได้ ตารางที่ตรวจแล้วข้างบนแสดงสี่เครื่องมือที่ไม่เห็นตรงกันบน input เดียวกันเป๊ะ:
tsc --noEmitบอก “ปกติดี” สำหรับenumกับ parameter property แต่node --experimental-strip-typesปฏิเสธไม่รันไฟล์เดียวกันเป๊ะ type-checking กับ runtime-runnability เป็นคำถามคนละเรื่อง เว้นแต่erasableSyntaxOnlyจะผูกมันเข้าด้วยกัน esbuildtranspile TypeScript decorator ถูกต้อง เพราะมัน “รองรับ TypeScript” แค่ที่--targetต่ำพอเท่านั้น ที่ default ของ esbuild เอง scratch test ของคอร์สนี้ทิ้ง decorator syntax ไว้โดยไม่แตะเลย ทำให้เกิด runtimeSyntaxError— “esbuild รองรับ X” มักแปลว่า “esbuild รองรับ X ที่ target ที่คุณตั้งไว้” ไม่ใช่แบบไม่มีเงื่อนไขtsc -bที่รายงาน error ในโปรเจกต์ upstream จะหยุดตรงนั้น โดย default ไม่หยุด — มันเดินต่อเข้าไปในโปรเจกต์ที่ depend อยู่ ใช้ build ล่าสุดที่ดีของมัน ทำให้ทุก failure ข้ามทั้ง graph โผล่มาในรอบเดียว--stopBuildOnErrorsคืนพฤติกรรม fail-fast แบบเดิม; ถ้าไม่ใส่ “app” ยังถูกพยายาม build แม้ “core” (dependency ของมัน) เพิ่งพังไป- error ของ
isolatedDeclarationsเป็นเรื่อง type ที่หายไปทั่วไป จริง ๆ มันเจาะจงเรื่อง return/property type ที่ infer มา ที่ checker สร้างใหม่ไม่ได้โดยไม่รัน inference เต็มรูปแบบ — ตรวจแล้วข้างบน code จริงคือTS9013(“Expression type can’t be inferred”) ไม่ใช่ code ที่ดูเหมือนจะทั่วไปกว่าที่เดาง่าย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- code ที่ generate มาเพิ่ม
enum,namespaceที่มี logic จริง, หรือ parameter property ใน codebase ที่ตั้งใจให้รันภายใต้--experimental-strip-typesของ Node หรือเครื่องมือแบบ strip-only ไหม?tsc --noEmitเพียว ๆ จะไม่เตือนเรื่องนี้เลย — มีแค่erasableSyntaxOnly(หรือลองรันไฟล์จริงกับ stripper เป้าหมาย) เท่านั้นที่จับได้ - มีการเพิ่ม decorator ในไฟล์ พร้อมสมมติว่า build step ที่เปลี่ยนจะ “ทำงานได้เลย” เพราะ “bundler จัดการ TypeScript ให้”? ตรวจว่า target ที่ตั้งไว้ของ bundler จริง ๆ lower decorator หรือเปล่า — target แบบ
esnext-ish default มักไม่ทำ และความพังจะโผล่แค่ตอน runtime ไม่ใช่ตอน build - CI step ของ
tsc -bที่ generate มา ถือว่า exit code ที่ไม่ใช่ศูนย์คือ “ทุกอย่างพัง” ทั้งที่ log จริงแสดง error จริงของโปรเจกต์เดียว กับโปรเจกต์อื่นที่แค่ถูกข้ามหรือ rebuild จาก output upstream ที่เก่าไหม? อ่านให้เห็นว่าโปรเจกต์ไหนรายงานerror TSxxxxจริง ก่อนจะสรุปว่าทั้ง graph พังเท่ากัน - ฟังก์ชันที่ตั้งใจ ship เป็น public API ของ library ถูกปล่อยไว้ไม่มี return type ชัดเจน ในโปรเจกต์ที่เปิด
isolatedDeclarationsเพื่อ emit.d.tsแบบขนานเร็ว ๆ ไหม? ช่องว่างนั้นเจาะจงคือสิ่งที่TS9013มีไว้จับ — ตรวจ code จริงก่อนจะอธิบายวิธีแก้ใน review comment
🐛 หา bug ในโค้ดนี้
export function createLogger(prefix: string) {return { info: (msg: string) => console.log(`[${prefix}] ${msg}`), error: (msg: string) => console.error(`[${prefix}] ERROR: ${msg}`),};}
export const enum Level {Info,Error,}เฉลย
สองปัญหาแยกกัน และ tsc --noEmit --strict รายงาน 0 error ทั้งคู่ อย่างแรก createLogger ไม่มี return type ชัดเจน — ภายใต้ strict เพียว ๆ ไม่มีปัญหา (checker infer ให้) แต่ทันทีที่ library นี้เปิด isolatedDeclarations เพื่อ emit .d.ts แบบขนานเร็ว ๆ shape นี้เป๊ะจะพังด้วย TS9013 เพราะ return type ที่ infer มาเป็น object literal ที่ checker ต้อง evaluate เต็มรูปแบบ ไม่ใช่แค่อ่านจาก syntax อย่างที่สอง const enum เข้มงวดกว่า enum ธรรมดาอีก: มันถูก inline เต็มที่ทุกจุดที่ใช้ภายใน program เดียวกัน ซึ่งแปลว่า const enum ที่ export ข้าม package boundary อาจ stale เงียบ ๆ ได้ถ้า consumer กับ library ถูก compile แยกกัน (การ generate .d.ts แบบแยก, incremental build) — และมันถูกปฏิเสธตรง ๆ ภายใต้ erasableSyntaxOnly เหมือน enum ธรรมดาเป๊ะ เพราะจากมุมมองของ runtime มันยังเป็น syntax ที่ไม่ erasable อยู่ดี การ review library code ที่ generate มาต้องตรวจทั้งสองอย่าง: ทุก export มี type ชัดเจนสำหรับ isolatedDeclarations ไหม และ enum/const enum ตัวไหนต้องกลายเป็น object แบบ as const แทน สำหรับโปรเจกต์ที่รันภายใต้ stripper