Migration Strategy
migrate ทีละส่วน ไม่ใช่ทีเดียว
หัวข้อที่มีชื่อว่า “migrate ทีละส่วน ไม่ใช่ทีเดียว”การ rewrite JS codebase จริงไป TypeScript ทีเดียวคือวิธีที่ migration ตายกัน จุดสำคัญของ TypeScript คือเป็น type system แบบ gradual: JS และ TS อยู่ด้วยกันได้ทีละไฟล์ คุณจึงแปลงทีละนิดขณะที่ยัง ship ได้ตลอด
เส้นทางแบบเป็น phase
หัวข้อที่มีชื่อว่า “เส้นทางแบบเป็น phase”flowchart LR js["allowJs: TS builds alongside JS"] --> checkjs["checkJs: type-check the JS too"] checkjs --> convert["rename .js to .ts file by file"] convert --> strict["ratchet strict flags one at a time"]
- เปิด
allowJsตอนนี้ TypeScript compile project แบบผสมได้ — ไฟล์.jsbuild เหมือนเดิม และคุณเริ่มเพิ่มไฟล์.tsข้าง ๆ ได้ - เพิ่ม
checkJs(และ// @ts-check) TypeScript type-check JavaScript ของคุณด้วย inference และ JSDoc คุณได้ error จริงก่อนแปลงไฟล์สักไฟล์ — first pass ฟรี ๆ - แปลงทีละไฟล์ rename
.jsเป็น.ts, แก้ error ที่โผล่, commit เริ่มจาก leaf (utility ที่ dependency น้อย) แล้วไล่เข้าหา core - ratchet strictness
strictเป็น default ตั้งแต่ TypeScript 6.0 โปรเจกต์ที่กำลัง migrate จึงเริ่มด้วย"strict": falseอย่างชัดเจน แล้วค่อยถอดออกเป็นขั้น — ลำดับที่แน่นอนสำคัญมาก และมี section ของตัวเองข้างล่าง
แต่ละ step ship ได้ คุณไม่เคยติดอยู่ในสภาพ migrate ค้างครึ่ง ๆ นาน ๆ
step ศูนย์: allowJs + checkJs + JSDoc ก่อน rename อะไรเลย
หัวข้อที่มีชื่อว่า “step ศูนย์: allowJs + checkJs + JSDoc ก่อน rename อะไรเลย”การ type-check จริงครั้งแรกที่ codebase ที่กำลัง migrate จะได้รับ ไม่ต้องแปลงไฟล์สักไฟล์เลย checkJs อ่าน JSDoc comment มาตรฐานเป็น type annotation บนไฟล์ .js ธรรมดา:
// @ts-check
/** * @param {number} a * @param {number} b * @returns {number} */function add(a, b) { return a + b;}
add(1, "two"); // should be flagged under checkJsรันไฟล์นั้นตรง ๆ ด้วย tsc --allowJs --checkJs --noEmit math.js แล้วมันรายงาน error จริงที่บรรทัดสุดท้าย: TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. (ตรวจแล้วใน scratch — เป็น diagnostic code เดียวกับที่ไฟล์ .ts จะได้ บนไฟล์ .js ที่ไม่แก้อะไรเลย) นี่คือ first pass ฟรี ๆ ที่ step 2 ข้างบนพูดถึง: คุณเพิ่ม JSDoc ให้ function ที่ทีมแตะบ่อยที่สุด เปิด checkJs แล้วได้ compile error จริง — รวมถึง code ที่ไม่มีใครดูมาหลายปี — ก่อนที่จะตัดสินใจ rename อะไรเลย มันยังเป็นจุดเริ่มต้นที่เป็นธรรมชาติสำหรับไฟล์ที่คุณยังไม่พร้อมแปลงเต็มที่: JSDoc verbose กว่า native TypeScript syntax แต่ไม่มี runtime cost เลย และใช้ในไฟล์ .js ธรรมดาได้ตลอดไป ถ้านั่นคือที่ที่ไฟล์นั้นต้องอยู่จริง ๆ
strictness ladder ตามลำดับ
หัวข้อที่มีชื่อว่า “strictness ladder ตามลำดับ”พอไฟล์เริ่มแปลงเป็น .ts strict คือ flag ตัวเดียวที่เปิด flag ย่อยเป็นสิบตัวพร้อมกัน — มากเกินไปสำหรับ codebase จริงส่วนใหญ่ในก้าวเดียว โปรเจกต์ที่ migrate ตั้ง `“strict”: false” ชัดเจนแล้วเรียง flag ย่อยกลับเข้าไปทีละชั้น และลำดับสำคัญมาก เพราะ error count กับความยากในการแก้ของแต่ละ flag ต่างกันมาก:
noImplicitAnyก่อน นี่มักเป็น wave ของ error ที่ใหญ่ที่สุด แต่ก็เป็น mechanical ที่สุดในการแก้ — ส่วนใหญ่แค่ parameter หรือ return type ที่หายไป ไม่ใช่ปัญหาด้าน design การแก้ตัวนี้ก่อนแปลว่า flag ทุกตัวหลังจากนี้กำลัง reasoning เกี่ยวกับ type จริง ไม่ใช่anyเงียบ ๆ ที่บัง shape จริงไว้strictNullChecksที่สอง นี่มักเป็น wave ที่ยากที่สุด — ทุกจุดที่null/undefinedเคยถูกเมินแบบ implicit ตอนนี้ต้องการการตัดสินใจจริง (guard, default, assertion ที่พิสูจน์ได้) มันมาเป็นอันดับสอง หลังnoImplicitAnyเพื่อให้ตอนนี้คุณ narrow type จริงอยู่แล้ว ไม่ใช่anyที่จะบัง null error ไว้อยู่ดีstrictFunctionTypesกับstrictBindCallApplyถัดไป จับ function-type assignment ที่ unsound และการใช้.call/.apply/.bindที่ type ผิด — ขอบเขตแคบกว่าสองตัวแรก และแทบไม่ต้องเปลี่ยน design เลย แค่แก้ signaturestrictPropertyInitializationสำหรับ class ตัวนี้ตั้งใจไว้ทีหลัง เพราะมันมักโผล่คำถามด้าน architecture จริง (field นี้ควร optional ไหม? assign ใน constructor? มี default ไหม?) มากกว่าจะแก้บรรทัดเดียวจบ — ตอบคำถามพวกนี้ทีหลังดีกว่า หลังจาก wave ที่ง่ายกว่าผ่านไปแล้ว- ที่เหลือ (
noImplicitThis,alwaysStrict,useUnknownInCatchVariables, …) ท้ายสุด โดยทั่วไปเป็น flag ที่ error count ต่ำ ทำความสะอาดขอบที่เหลือหลังจาก wave โครงสร้างใหญ่จบไปแล้ว
พอ flag ตัวสุดท้ายเปิดแล้ว ลบบรรทัด strict: false ทิ้งไปเลย โปรเจกต์ก็อยู่บน default จริง ไม่ใช่การประกอบ default เองด้วยมือ
@ts-nocheck ต่อไฟล์: escape hatch ของ ratchet ใช้แต่น้อย
หัวข้อที่มีชื่อว่า “@ts-nocheck ต่อไฟล์: escape hatch ของ ratchet ใช้แต่น้อย”บางไฟล์ต่อต้านการแปลง — generated code, module ที่ dynamic จริง ๆ, อะไรที่กำหนดให้ลบทิ้ง // @ts-nocheck เป็นบรรทัดแรก suppress error ทุกตัวในไฟล์นั้น ปล่อยให้ที่เหลือของ project ratchet ต่อไปโดยไม่ต้องรอมัน:
// @ts-nocheckconst total: number = "not a number"; // would be TS2322 -- suppressed for this whole fileexport function broken(x: string): number { return x; // would also error -- also suppressed}@ts-nocheck hides every error in the file, real or not
// @ts-nocheckconst total: number = "not a number"; // would be TS2322 -- suppressed for this whole fileexport function broken(x: string): number { return x; // would also error -- also suppressed} error จริงทั้งสองตัวข้างบนหายไปหมด — tsc --strict รายงานปัญหาเป็นศูนย์สำหรับไฟล์นี้ ซึ่งคืออันตรายพอดี: @ts-nocheck คือค้อนที่ใหญ่กว่า @ts-expect-error มาก เพราะมัน silence ทั้งไฟล์ รวมถึง bug ที่ไม่มีใครดูเลย ไม่ใช่แค่บรรทัดเดียวที่คุณตั้งใจข้าม มองมันเป็นข้อยกเว้นที่ track ไว้และมีชื่อ — comment ที่ link ไป ticket, ไฟล์ที่ถูก list ไว้ชัดเจนใน migration tracker — ไม่ใช่ default เงียบ ๆ สำหรับ “ไฟล์นี้มีปัญหา”
การวัดความคืบหน้า
หัวข้อที่มีชื่อว่า “การวัดความคืบหน้า”migration ต้องการตัวเลขที่วิ่งไปทางเดียว ไม่งั้นจะติดตามยากว่าคุณกำลัง converge จริงหรือเปล่า metric ที่ grep เจอจริงสองตัวใช้ร่วมกันได้ดี: จำนวนไฟล์ที่ยังเป็น @ts-nocheck (ไฟล์ที่ไม่ถูก check เลย) และสัดส่วนไฟล์ .ts/.tsx เทียบกับทั้งหมด:
total=$(find src -name '*.ts' -o -name '*.tsx' | wc -l | tr -d ' ')nocheck=$(grep -rl '^// @ts-nocheck' src --include='*.ts' --include='*.tsx' | wc -l | tr -d ' ')echo "$((total - nocheck)) / $total files fully type-checked"(ตรวจแล้วว่าเป็น shell logic จริง กับ scratch project สามไฟล์ — ไฟล์หนึ่งมี // @ts-nocheck นำหน้า อีกสองไฟล์ไม่มี — มันพิมพ์ 2 / 3 files fully type-checked ถูกต้อง) track ตัวเลขนี้ (หรือจำนวน any จากบท performance-and-pitfalls) ใน CI หรือแค่ในการ check-in ของทีมตามรอบ — migration ที่ไม่มี metric ที่มองเห็นได้มักจะหยุดนิ่งเงียบ ๆ พอความกระตือรือร้นแรก ๆ จางลง เพราะไม่มีอะไรแสดงว่างานที่เหลือกำลังเล็กลง
any เป็น tool ก่อน แล้วเป็นหนี้
หัวข้อที่มีชื่อว่า “any เป็น tool ก่อน แล้วเป็นหนี้”ระหว่าง migrate any เป็นวิธีที่ถูกต้องในการบอกว่า “ยังไม่ type — ไปต่อก่อน” อันตรายคือปล่อยทิ้งไว้ วินัยสองข้อช่วยให้ any ซื่อสัตย์:
- ทำให้ explicit และ grep เจอ เลือก
anyที่ explicit (หรือ alias// TODO: type this) แทน implicit เพื่อให้หาและกำจัดทีหลังได้ - ratchet ด้วย
noImplicitAnyพอเปิด compiler เลิกแอบใส่any— code ใหม่ต้อง type หนี้จึงมีแต่ลดลง
@ts-expect-error ดีกว่า @ts-ignore
หัวข้อที่มีชื่อว่า “@ts-expect-error ดีกว่า @ts-ignore”เมื่อต้อง suppress error ให้ใช้ @ts-expect-error ไม่ใช่ @ts-ignore:
// @ts-expect-error — legacy shape, tracked in TICKET-123legacyCall(weirdValue);ทั้งคู่ silence error ในบรรทัดถัดไป แต่ @ts-expect-error จะ error ด้วยถ้าบรรทัดนั้นเลิกมี error ดังนั้นเมื่อคุณแก้ type ที่เป็นต้นเหตุทีหลัง suppression ที่ไม่จำเป็นแล้วจะทำให้ build พัง และบอกให้คุณลบทิ้ง ส่วน @ts-ignore แค่เน่าเงียบ ๆ @ts-expect-error คือเครื่องมือขนาดพอดีสำหรับบรรทัดเดียว; @ts-nocheck (ข้างบน) คือเครื่องมือที่ทื่อกว่ามากสำหรับทั้งไฟล์ และสองตัวนี้ไม่ควรถูกหยิบมาใช้แทนกันเอง
import type กับ barrel-file cycle
หัวข้อที่มีชื่อว่า “import type กับ barrel-file cycle”ภายใต้ verbatimModuleSyntax (ตัวแทนยุคใหม่ของ check ที่เกี่ยวกับ import ของ isolatedModules) import ที่ถูกใช้เป็น type อย่างเดียวต้องบอกให้ชัดเจน:
// @skip-verify multi-file example -- ./types.ts exports `interface User` and `function makeUser`; this is ./main.tsimport { User, makeUser } from "./types"; // mixes a type and a value in one importconst u: User = makeUser(1, "Ada");โค้ดนี้พังภายใต้ verbatimModuleSyntax ด้วย TS1484: 'User' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled. (ตรวจแล้วใน scratch project สองไฟล์จริง — tsc --verbatimModuleSyntax --strict main.ts กับ ./types.ts ตัวนั้นเป๊ะ) การแยก import แก้ปัญหาและบอก intent ให้ฟรี:
// @skip-verify multi-file example -- ./types.ts exports `interface User` and `function makeUser`import { makeUser } from "./types";import type { User } from "./types"; // type-only import survives verbatimModuleSyntax
const u: User = makeUser(1, "Ada");ค่าของ import type (และ export type คู่กัน) ไม่ใช่แค่ทำให้ flag พอใจ — type-only import การันตีว่าจะถูก erase ตอน emit ดังนั้นมันไม่มีทางสร้าง runtime dependency ระหว่าง module ที่ต้องการแค่แชร์ shape กันเท่านั้นโดยไม่ตั้งใจ นั่นสำคัญที่สุดสำหรับ barrel-file cycle: module a re-export จาก barrel index.ts ของ module b ซึ่ง re-export จาก barrel ของ module a กลับมาอีกที และถ้าฝั่งไหนดึงอีกฝั่งเข้ามาสำหรับ type ที่มันต้องการแค่เพื่อ annotation runtime circular-import จริง (value ที่ undefined ตอน import, พังตามลำดับ) อาจโผล่ขึ้นมาในที่ที่ตั้งใจให้เป็นแค่ความสัมพันธ์ระดับ type ตอน compile-time เท่านั้น การ migrate codebase ไปใช้ import type สำหรับอะไรก็ตามที่เป็น type-only จริง ๆ กำจัด bug class ทั้งชุดของ runtime coupling ที่ไม่ตั้งใจนี้ไปเลย — compiler บังคับ ทีละ module ว่า import ที่ “แค่ type” จะไม่มีทางแอบสร้าง dependency จริงได้
migrate ไป TypeScript 6 และ 7
หัวข้อที่มีชื่อว่า “migrate ไป TypeScript 6 และ 7”migration อีกแบบที่คุณจะได้ทำปีนี้คือการ upgrade โปรเจกต์ TypeScript ที่มีอยู่ 6.0 (มีนาคม 2026) deprecate option ชุดหนึ่งและเปลี่ยน default; 7.0 (กรกฎาคม 2026) ถอด option ที่ deprecate ออกและสลับเป็น native compiler ทำเป็นสองก้าว:
- upgrade เป็น 6.0 ก่อน แล้วเพิ่ม
"ignoreDeprecations": "6.0"ในtsconfig.jsonbuild กลับมาทำงานทันที และ deprecation กลายเป็น warning ที่คุณค่อย ๆ กำจัดตามจังหวะตัวเองแทนที่จะเป็นกำแพง error - แก้ default ที่ขยับไปใต้เท้าคุณ ตั้ง
"types": ["node", "jest", …]ให้ชัด (default ตอนนี้คือ[]), ตั้ง"rootDir": "./src"ถ้าเคยพึ่ง inference และเช็คว่าstrict: trueกับmodule: esnextคือสิ่งที่คุณต้องการ — โปรเจกต์ CJS ตั้งmodule: commonjs(หรือnodenextพร้อม"type": "commonjs") อย่างตั้งใจ - แทน option ที่ deprecate ทีละตัว: ยุบ
baseUrlเข้าpaths("@app/*": ["./src/app/*"]);moduleResolution: node→nodenextหรือbundler; เลิกใช้outFileแล้วใช้ bundler; ยกtargetขึ้นจากes5และลบdownlevelIteration; ลบบรรทัดesModuleInterop/allowSyntheticDefaultImports/alwaysStrictที่ตั้งเป็นfalse(ตอนนี้เปิดตลอดแล้ว); เปลี่ยนimport … assert { type: "json" }เป็นwith { type: "json" }และmodule Foo {}เป็นnamespace Foo {} - แก้ code ที่พึ่งพา interop แบบเก่า:
import * as express from "express"กลายเป็นimport express from "express"และ script ที่เคยรันtsc some-file.tsข้าง ๆ tsconfig ต้องใส่--ignoreConfig - ถอด
ignoreDeprecationsออก แล้ว upgrade เป็น 7.0 ถ้า build สะอาดบน 6.0 โดยไม่มีมัน 7.0 คือ drop-in รันtsc --stableTypeOrderingบน 6.0 ก่อนถ้าคุณ diff output.d.tsใน CI — 7 เรียง member ของ union แบบ deterministic และ flag นี้ให้คุณเห็นลำดับใหม่ก่อนสลับ - เก็บ 6.0 ไว้สำหรับเครื่องมือที่ต้องใช้ API
typescript-eslint, Volar (Vue) และ language tooling ของ Svelte/Astro/Angular ยังต้องใช้ programmatic API ซึ่ง 7.0 ไม่ ship จนกว่า 7.1 ติดตั้ง@typescript/typescript6คู่กับ 7 แล้วชี้เครื่องมือพวกนั้นไปที่มัน; ใน VS Code language server ทั้งสองอยู่ด้วยกันได้ และ “Disable TypeScript 7 Language Server” ถอยกลับไป 6 สำหรับ workspace ที่ต้องการ
ทุกก้าว ship ได้ ซึ่งเป็นหลักการเดียวกับเส้นทาง JS ไป TS ข้างบน: อย่าอยู่ในสภาพที่ build พังนานเกินหนึ่งวัน
การเขียน library
หัวข้อที่มีชื่อว่า “การเขียน library”ถ้าคุณ publish package มีกฎเพิ่มอีกไม่กี่ข้อ:
- ship declaration file (
.d.ts) ตั้ง"declaration": trueเพื่อให้ผู้ใช้ได้ type ของคุณ ชี้"types"ในpackage.jsonไปที่ entry.d.ts - อย่าให้ internal type leak เฉพาะ type ของ public API เท่านั้นที่เป็น contract เก็บ type ของ implementation ไว้ไม่ export เพื่อให้เปลี่ยนได้อย่างอิสระ
- มอง type เป็น semver การเปลี่ยนที่ทำให้ type checking ของผู้ใช้พังคือ breaking change แม้ runtime behavior จะเหมือนเดิม การ widen parameter มักปลอดภัย; การ narrow parameter หรือเปลี่ยน return type ทำให้ build ของ downstream พังได้
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกเทคนิคในบทนี้มีไว้เพื่อตอบคำถามเดียวอย่างซื่อสัตย์ในทุกจุดของ migration ที่ยาวนาน: code ชิ้นนี้ถูก check อยู่ตอนนี้หรือเปล่า — และฉันบอกได้ไหม? allowJs/checkJs ขยายคำว่า “ถูก check” ให้ไกลที่สุดก่อนที่จะ rename อะไรเลย strictness ladder ถูกเรียงเพื่อให้แต่ละ wave ของ code ที่เพิ่งถูก check เป็น type จริง ไม่ใช่ any ที่จะบัง error ของ wave ถัดไป @ts-nocheck กับ @ts-expect-error ทั้งคู่คือ opt-out ที่ซื่อสัตย์ — ตั้งใจให้มองเห็นได้, grep เจอ, และในกรณีของ @ts-expect-error ลบตัวเองเมื่อไม่จำเป็นแล้ว — ตรงข้ามกับ any แบบ implicit หรือ @ts-ignore ที่ค้างเงียบ ๆ ขยาย set ของ “ไม่ถูก check” ไปเรื่อย ๆ โดยไม่มีใครสังเกต migration ไม่ได้ล้มเหลวเพราะ any มีอยู่ มันล้มเหลวเมื่อไม่มีใครบอกได้อีกแล้วว่า any ได้เงียบ ๆ ลามไปถึงไหนของ codebase แล้ว
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| แนวทาง | ข้อดี | ต้นทุน |
|---|---|---|
JSDoc + checkJs บนไฟล์ .js | typing ไม่มี runtime cost ทำงานได้โดยไม่ต้อง rename หรือเปลี่ยน build step | verbose กว่า native TS syntax; type feature ขั้นสูงบางอย่างเขียนยากหรือเป็นไปไม่ได้ |
แปลงเป็น .ts ทันที | syntax native เต็มรูปแบบ ไม่ verbose แบบ JSDoc | ต้อง rename + เปลี่ยน build step ต่อไฟล์; เสี่ยงกว่าสำหรับไฟล์ที่ยังไม่พร้อม commit เต็มที่ |
strict ทีเดียว | เส้นทางเร็วที่สุดไปสู่ safety เต็มรูปแบบ ไม่ต้องดูแล ladder | มักเป็นกำแพง error ที่จัดการไม่ไหวบน codebase จริงส่วนใหญ่ — เหตุผลที่บทนี้แนะนำ ladder ตั้งแต่แรก |
@ts-expect-error | ลบตัวเองเมื่อ type ที่เป็นต้นเหตุถูกแก้ — เน่าเงียบ ๆ ไม่ได้ | suppress ได้แค่บรรทัดเดียว; ไม่ scale ไปถึง “ทั้งไฟล์นี้ยังไม่พร้อม” |
@ts-nocheck | ปลดล็อกทั้งไฟล์ที่ดื้อได้ทันที | ซ่อน error ทุกตัวในไฟล์นั้น รวมถึงตัวใหม่ที่เพิ่มเข้ามาทีหลัง — เครื่องมือที่ทื่อที่สุดในบทนี้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”checkJsต้องแปลงไฟล์เป็น.tsก่อน ไม่จริง — มัน type-check ไฟล์.jsธรรมดาด้วย inference และ JSDoc โดยไม่ต้อง rename อะไรเลย ซึ่งเป็นเหตุผลที่มันควรอยู่ที่ step ศูนย์- ลำดับที่เปิด flag ย่อยของ
strictไม่สำคัญ ตราบใดที่สุดท้ายไปถึงstrict: trueมันสำคัญมากต่อว่าแต่ละ wave ของ error จัดการง่ายแค่ไหน —noImplicitAnyก่อนstrictNullChecksแปลว่าตอนที่ wave null-safety ที่ยากกว่ามาถึง คุณกำลัง narrow type จริงอยู่แล้ว ไม่ใช่any @ts-nocheckกับ@ts-ignore/@ts-expect-errorเป็นเครื่องมือแบบเดียวกันแค่คนละ scope@ts-expect-errorแก้ตัวเองได้ (error เมื่อไม่จำเป็นแล้ว);@ts-ignoreกับ@ts-nocheckไม่ทำแบบนั้น — ทั้งคู่เน่าเงียบ ๆ ได้ และ@ts-nocheckทำแบบนั้นกับทั้งไฟล์ทีเดียวimport typeเป็นแค่ style preference สำหรับ code ที่สะอาดกว่า ภายใต้verbatimModuleSyntaxมันถูกบังคับโดย compiler และที่สำคัญกว่านั้น มันคือการันตีจริงว่าจะไม่มีทางสร้าง runtime circular-import โดยไม่ตั้งใจ จากสิ่งที่ตั้งใจให้เป็นแค่ความสัมพันธ์ระดับ type ตอน compile-time — โดยเฉพาะข้าม barrel file
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ก่อนเชื่อ migration step ที่ generate มา หรือ “quick fix” สำหรับ type error ตรวจสอบ:
@ts-nocheckที่ถูกเพิ่มเพื่อให้ error ของไฟล์ที่ดื้อหายไป โดยไม่มี ticket reference หรือคำอธิบาย — ตรวจดูว่ามัน suppress error จริงอะไรอยู่ก่อนจะยอมรับ- flag ย่อยของ
strictถูกเปิดพร้อมกันทั้งหมดใน diff ที่ generate มาครั้งเดียวบน codebase ใหญ่ที่มีอยู่แล้ว — ถามว่าแต่ละ wave ของ error ที่ตามมาถูก review จริงหรือแค่ suppress รวดเดียวเพื่อให้ build ผ่าน import { Foo, useFoo }ที่ generate มาโดยFooถูกใช้เป็น type อย่างเดียว — ภายใต้verbatimModuleSyntaxควรถูกแยก ถ้าไม่ถูกแยก เป็นสัญญาณว่าการ generate ไม่ได้รัน type checker กับ config จริงจริง ๆ- คำแนะนำให้ “ใช้
@ts-ignoreไปเลย” สำหรับ error ที่จริง ๆ เป็น type mismatch ที่แก้ได้จริง ไม่ใช่ข้อจำกัดจริงของ type system - การเปลี่ยนแปลงของ library ที่ ship
.d.tsที่ถูกอธิบายว่า “non-breaking” แต่จริง ๆ narrow parameter type หรือ widen return type — ตรวจกับกฎ semver ในบทนี้ ไม่ใช่แค่ runtime behavior
🐛 หา bug ในโค้ดนี้
// @ts-nocheckexport function parseConfig(raw) {const data = JSON.parse(raw);return { port: data.port, host: data.host.toLowerCase(),};}เฉลย
ไฟล์นี้ compile ผ่านไม่มี error เลย และจะยัง compile ผ่านไม่มี error เลยไม่ว่าข้างในจะพังแค่ไหน — นั่นคือประเด็นทั้งหมด (และอันตรายทั้งหมด) ของ @ts-nocheck บรรทัดแรก data.host.toLowerCase() จะ throw ตอน runtime ถ้า host หายไปจาก config ที่ parse มา data.port ไม่มี type เลย (มันคืออะไรก็ตามที่ JSON.parse สร้างมา any) และถ้ามีใครเพิ่ม typo จริง ๆ ทีหลัง (dat.port แทน data.port) compiler ก็จะไม่พูดอะไรเกี่ยวกับมันเลยเหมือนกัน — ไฟล์นี้อยู่นอกการป้องกันทั้งหมดตลอดไป จนกว่าจะมีคนลบ directive ออก ใน PR การเจอ @ts-nocheck เดี่ยว ๆ บนไฟล์ที่มี logic จริงกำลังทำงานอยู่แบบนี้ (ไม่ใช่ generated code หรือไฟล์ที่รอลบ) ควรถูกมองว่าเป็นการขอให้อธิบายว่าทำไมไฟล์นี้ถึง type-check ไม่ได้เลย ไม่ใช่วิธีปกติในการ silence error ที่น่ารำคาญ