ข้ามไปยังเนื้อหา

Migration Strategy

การ rewrite JS codebase จริงไป TypeScript ทีเดียวคือวิธีที่ migration ตายกัน จุดสำคัญของ TypeScript คือเป็น type system แบบ gradual: JS และ TS อยู่ด้วยกันได้ทีละไฟล์ คุณจึงแปลงทีละนิดขณะที่ยัง ship ได้ตลอด

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"]
ratchet การ migrate แบบค่อยเป็นค่อยไป
  1. เปิด allowJs ตอนนี้ TypeScript compile project แบบผสมได้ — ไฟล์ .js build เหมือนเดิม และคุณเริ่มเพิ่มไฟล์ .ts ข้าง ๆ ได้
  2. เพิ่ม checkJs (และ // @ts-check) TypeScript type-check JavaScript ของคุณด้วย inference และ JSDoc คุณได้ error จริงก่อนแปลงไฟล์สักไฟล์ — first pass ฟรี ๆ
  3. แปลงทีละไฟล์ rename .js เป็น .ts, แก้ error ที่โผล่, commit เริ่มจาก leaf (utility ที่ dependency น้อย) แล้วไล่เข้าหา core
  4. ratchet strictness strict เป็น default ตั้งแต่ TypeScript 6.0 โปรเจกต์ที่กำลัง migrate จึงเริ่มด้วย "strict": false อย่างชัดเจน แล้วค่อยถอดออกเป็นขั้น — ลำดับที่แน่นอนสำคัญมาก และมี section ของตัวเองข้างล่าง

แต่ละ step ship ได้ คุณไม่เคยติดอยู่ในสภาพ migrate ค้างครึ่ง ๆ นาน ๆ

การ 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 ธรรมดาได้ตลอดไป ถ้านั่นคือที่ที่ไฟล์นั้นต้องอยู่จริง ๆ

พอไฟล์เริ่มแปลงเป็น .ts strict คือ flag ตัวเดียวที่เปิด flag ย่อยเป็นสิบตัวพร้อมกัน — มากเกินไปสำหรับ codebase จริงส่วนใหญ่ในก้าวเดียว โปรเจกต์ที่ migrate ตั้ง `“strict”: false” ชัดเจนแล้วเรียง flag ย่อยกลับเข้าไปทีละชั้น และลำดับสำคัญมาก เพราะ error count กับความยากในการแก้ของแต่ละ flag ต่างกันมาก:

  1. noImplicitAny ก่อน นี่มักเป็น wave ของ error ที่ใหญ่ที่สุด แต่ก็เป็น mechanical ที่สุดในการแก้ — ส่วนใหญ่แค่ parameter หรือ return type ที่หายไป ไม่ใช่ปัญหาด้าน design การแก้ตัวนี้ก่อนแปลว่า flag ทุกตัวหลังจากนี้กำลัง reasoning เกี่ยวกับ type จริง ไม่ใช่ any เงียบ ๆ ที่บัง shape จริงไว้
  2. strictNullChecks ที่สอง นี่มักเป็น wave ที่ยากที่สุด — ทุกจุดที่ null/undefined เคยถูกเมินแบบ implicit ตอนนี้ต้องการการตัดสินใจจริง (guard, default, assertion ที่พิสูจน์ได้) มันมาเป็นอันดับสอง หลัง noImplicitAny เพื่อให้ตอนนี้คุณ narrow type จริงอยู่แล้ว ไม่ใช่ any ที่จะบัง null error ไว้อยู่ดี
  3. strictFunctionTypes กับ strictBindCallApply ถัดไป จับ function-type assignment ที่ unsound และการใช้ .call/.apply/.bind ที่ type ผิด — ขอบเขตแคบกว่าสองตัวแรก และแทบไม่ต้องเปลี่ยน design เลย แค่แก้ signature
  4. strictPropertyInitialization สำหรับ class ตัวนี้ตั้งใจไว้ทีหลัง เพราะมันมักโผล่คำถามด้าน architecture จริง (field นี้ควร optional ไหม? assign ใน constructor? มี default ไหม?) มากกว่าจะแก้บรรทัดเดียวจบ — ตอบคำถามพวกนี้ทีหลังดีกว่า หลังจาก wave ที่ง่ายกว่าผ่านไปแล้ว
  5. ที่เหลือ (noImplicitThis, alwaysStrict, useUnknownInCatchVariables, …) ท้ายสุด โดยทั่วไปเป็น flag ที่ error count ต่ำ ทำความสะอาดขอบที่เหลือหลังจาก wave โครงสร้างใหญ่จบไปแล้ว

พอ flag ตัวสุดท้ายเปิดแล้ว ลบบรรทัด strict: false ทิ้งไปเลย โปรเจกต์ก็อยู่บน default จริง ไม่ใช่การประกอบ default เองด้วยมือ

บางไฟล์ต่อต้านการแปลง — generated code, module ที่ dynamic จริง ๆ, อะไรที่กำหนดให้ลบทิ้ง // @ts-nocheck เป็นบรรทัดแรก suppress error ทุกตัวในไฟล์นั้น ปล่อยให้ที่เหลือของ project ratchet ต่อไปโดยไม่ต้องรอมัน:

// @ts-nocheck
const total: number = "not a number"; // would be TS2322 -- suppressed for this whole file
export function broken(x: string): number {
return x; // would also error -- also suppressed
}

@ts-nocheck hides every error in the file, real or not

// @ts-nocheck
const total: number = "not a number"; // would be TS2322 -- suppressed for this whole file
export function broken(x: string): number {
return x; // would also error -- also suppressed
}
Open in TS Playground

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 เทียบกับทั้งหมด:

Terminal window
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 ที่มองเห็นได้มักจะหยุดนิ่งเงียบ ๆ พอความกระตือรือร้นแรก ๆ จางลง เพราะไม่มีอะไรแสดงว่างานที่เหลือกำลังเล็กลง

ระหว่าง migrate any เป็นวิธีที่ถูกต้องในการบอกว่า “ยังไม่ type — ไปต่อก่อน” อันตรายคือปล่อยทิ้งไว้ วินัยสองข้อช่วยให้ any ซื่อสัตย์:

  • ทำให้ explicit และ grep เจอ เลือก any ที่ explicit (หรือ alias // TODO: type this) แทน implicit เพื่อให้หาและกำจัดทีหลังได้
  • ratchet ด้วย noImplicitAny พอเปิด compiler เลิกแอบใส่ any — code ใหม่ต้อง type หนี้จึงมีแต่ลดลง

เมื่อต้อง suppress error ให้ใช้ @ts-expect-error ไม่ใช่ @ts-ignore:

// @ts-expect-error — legacy shape, tracked in TICKET-123
legacyCall(weirdValue);

ทั้งคู่ silence error ในบรรทัดถัดไป แต่ @ts-expect-error จะ error ด้วยถ้าบรรทัดนั้นเลิกมี error ดังนั้นเมื่อคุณแก้ type ที่เป็นต้นเหตุทีหลัง suppression ที่ไม่จำเป็นแล้วจะทำให้ build พัง และบอกให้คุณลบทิ้ง ส่วน @ts-ignore แค่เน่าเงียบ ๆ @ts-expect-error คือเครื่องมือขนาดพอดีสำหรับบรรทัดเดียว; @ts-nocheck (ข้างบน) คือเครื่องมือที่ทื่อกว่ามากสำหรับทั้งไฟล์ และสองตัวนี้ไม่ควรถูกหยิบมาใช้แทนกันเอง

ภายใต้ verbatimModuleSyntax (ตัวแทนยุคใหม่ของ check ที่เกี่ยวกับ import ของ isolatedModules) import ที่ถูกใช้เป็น type อย่างเดียวต้องบอกให้ชัดเจน:

// @skip-verify multi-file example -- ./types.ts exports `interface User` and `function makeUser`; this is ./main.ts
import { User, makeUser } from "./types"; // mixes a type and a value in one import
const 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 จริงได้

migration อีกแบบที่คุณจะได้ทำปีนี้คือการ upgrade โปรเจกต์ TypeScript ที่มีอยู่ 6.0 (มีนาคม 2026) deprecate option ชุดหนึ่งและเปลี่ยน default; 7.0 (กรกฎาคม 2026) ถอด option ที่ deprecate ออกและสลับเป็น native compiler ทำเป็นสองก้าว:

  1. upgrade เป็น 6.0 ก่อน แล้วเพิ่ม "ignoreDeprecations": "6.0" ใน tsconfig.json build กลับมาทำงานทันที และ deprecation กลายเป็น warning ที่คุณค่อย ๆ กำจัดตามจังหวะตัวเองแทนที่จะเป็นกำแพง error
  2. แก้ default ที่ขยับไปใต้เท้าคุณ ตั้ง "types": ["node", "jest", …] ให้ชัด (default ตอนนี้คือ []), ตั้ง "rootDir": "./src" ถ้าเคยพึ่ง inference และเช็คว่า strict: true กับ module: esnext คือสิ่งที่คุณต้องการ — โปรเจกต์ CJS ตั้ง module: commonjs (หรือ nodenext พร้อม "type": "commonjs") อย่างตั้งใจ
  3. แทน 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 {}
  4. แก้ code ที่พึ่งพา interop แบบเก่า: import * as express from "express" กลายเป็น import express from "express" และ script ที่เคยรัน tsc some-file.ts ข้าง ๆ tsconfig ต้องใส่ --ignoreConfig
  5. ถอด 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. เก็บ 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 พังนานเกินหนึ่งวัน

ถ้าคุณ 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 พังได้

ทุกเทคนิคในบทนี้มีไว้เพื่อตอบคำถามเดียวอย่างซื่อสัตย์ในทุกจุดของ 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 บนไฟล์ .jstyping ไม่มี runtime cost ทำงานได้โดยไม่ต้อง rename หรือเปลี่ยน build stepverbose กว่า 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

ก่อนเชื่อ 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-nocheck
export 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 ที่น่ารำคาญ

วิธีที่แนะนำในการ migrate JS codebase ไป TypeScript คืออะไร?
checkJs (พร้อม @ts-check) ให้อะไรก่อนแปลงไฟล์?
ทำไม strictness ladder ถึงเอา noImplicitAny มาก่อน strictNullChecks?
ทำไมควรเลือก @ts-expect-error มากกว่า @ts-ignore?
@ts-nocheck ทำอะไรตรงนี้ แล้วความเสี่ยงคืออะไร?
// @ts-nocheck
export function parseConfig(raw) {
  const data = JSON.parse(raw);
  return { port: data.port, host: data.host.toLowerCase() };
}
ภายใต้ verbatimModuleSyntax เกิดอะไรขึ้น และทำไม import type ถึงสำคัญมากกว่าแค่ทำให้ flag พอใจ?
import { User, makeUser } from "./types";
const u: User = makeUser(1, "Ada");