Modules & Resolution
module system สองแบบ ยังอยู่
หัวข้อที่มีชื่อว่า “module system สองแบบ ยังอยู่”JavaScript มี module format สองแบบ และ TypeScript ต้องรองรับทั้งคู่:
- CommonJS (CJS) — format เก่าของ Node:
require()และmodule.exportsโหลดแบบ synchronous ด้วยการ resolve ไฟล์บน disk - ES Modules (ESM) — มาตรฐาน:
importและexportใช้ native ใน browser และ Node สมัยใหม่ วิเคราะห์แบบ static ได้
ความสับสนส่วนใหญ่ในโปรเจกต์ TypeScript ย้อนกลับไปที่ความไม่ตรงกันระหว่าง module format ที่คุณ เขียน, format ที่ tsc emit และ format ที่ runtime คาดหวัง option module และ moduleResolution คือวิธีที่คุณทำให้ทั้งสามอย่างนี้ตรงกัน
resolution ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “resolution ทำงานอย่างไร”เมื่อคุณเขียน import { x } from "./util" tsc ต้องแปลง "./util" ให้เป็นไฟล์จริง กลยุทธ์ที่ใช้ขึ้นกับ moduleResolution:
flowchart TB q["How is your code loaded?"] --> node["Runs in Node directly"] q --> bundled["Goes through a bundler"] node --> nodenext["moduleResolution: NodeNext respects package.json type + exports"] bundled --> bundler["moduleResolution: Bundler lets the bundler resolve"]
NodeNext/Node16— จำลอง Node สมัยใหม่แบบเป๊ะ: อ่าน"type": "module"ในpackage.json, เคารพ field"exports"และบังคับกฎ ESM (รวมถึงต้องเขียน file extension ใน relative import:import "./util.js")Bundler— สำหรับ code ที่ผ่าน Vite, esbuild หรือ webpack โหมดนี้ผ่อนเรื่อง extension และปล่อยให้ bundler จัดการการหาไฟล์จริง ให้ตรงกับพฤติกรรมของเครื่องมือเหล่านั้นNode10(เดิมชื่อNode) — algorithm CommonJS แบบเก่า หลีกเลี่ยงในโปรเจกต์ใหม่ ไม่เข้าใจ field"exports"
mental model สำคัญ: moduleResolution ควรอธิบายว่า code ของคุณถูก โหลดจริง อย่างไร Node รันโดยตรงใช่ไหม? ใช้ NodeNext ผ่าน bundler ใช่ไหม? ใช้ Bundler
เรื่องเซอร์ไพรส์ของ extension
หัวข้อที่มีชื่อว่า “เรื่องเซอร์ไพรส์ของ extension”ภายใต้ NodeNext native ESM ต้องมี extension ใน import และ — น่าสับสน — คุณเขียน extension .js แม้ไฟล์บน disk จะเป็น .ts:
// util.ts exists on disk, but the emitted import must point at the emitted .jsimport { helper } from "./util.js"; // ✅ correct under NodeNext ESMimport { helper } from "./util"; // ❌ ESM needs the extensionเรื่องนี้ทำทุกคนสะดุดครั้งหนึ่ง เหตุผลคือ tsc ไม่ rewrite import path ของคุณ และตอน runtime ไฟล์จะเป็น util.js ดังนั้น source ต้องเรียกชื่อไฟล์ตอน runtime ไว้ตั้งแต่แรก
esModuleInterop
หัวข้อที่มีชื่อว่า “esModuleInterop”CJS กับ ESM มีความเห็นไม่ตรงกันเรื่อง default export ซึ่งในอดีตทำให้ import express from "express" fail กับ module ที่เป็น CommonJS esModuleInterop: true แทรก interop shim เล็ก ๆ ที่ทำให้ default และ namespace import ของ CommonJS module ทำงานตามที่คุณคาดหวัง flag นี้เปิดเป็น default ใน config ที่แนะนำ และคุณเกือบจะอยากได้เสมอ — การปิดคือแหล่งของ import error ที่เลี่ยงได้