tsconfig, Deep
default เปลี่ยนไปใน TypeScript 6
หัวข้อที่มีชื่อว่า “default เปลี่ยนไปใน TypeScript 6”ตลอดสิบปี tsconfig.json ที่ว่างเปล่าแปลว่า compiler แบบ หลวม: ไม่มี strict, output เป็น CommonJS, ES target เก่า และ package @types/* ทุกตัวใน node_modules ถูกโหลดเงียบ ๆ TypeScript 6.0 (มีนาคม 2026) พลิก default ให้เป็นสิ่งที่แทบทุกโปรเจกต์เขียนเองอยู่แล้ว และ TypeScript 7.0 (กรกฎาคม 2026, native compiler) ทำให้มันเป็นของถาวร:
| Option | ก่อน 6.0 | default ใน 6.0 / 7.0 |
|---|---|---|
strict | false | true |
module | commonjs | esnext |
target | ประมาณ es2020 | es2025 — ขยับตาม ECMAScript ของปีปัจจุบัน |
types | @types/* ทุกตัวใน node_modules | [] — ไม่โหลดอะไรเลยนอกจากที่คุณระบุ |
rootDir | infer จาก source ของคุณ | . — directory ของ tsconfig.json |
noUncheckedSideEffectImports | false | true |
libReplacement | true | false |
สองตัวในนี้กัดโปรเจกต์จริงตอน upgrade types: [] แปลว่า @types/node ไม่ถูกหยิบมาใช้แค่เพราะ install ไว้ — ต้องเขียน "types": ["node"] (หรือ ["*"] ถ้าอยากได้พฤติกรรมเดิม) ส่วน rootDir: "." แปลว่า layout แบบ src/ จะ emit ไปที่ dist/src/ ถ้าไม่ตั้ง "rootDir": "./src" ให้ชัด — และเรื่องนี้ไม่เงียบ: ถ้าตั้ง outDir ไว้แต่ปล่อย rootDir เป็น default tsc จะหยุดด้วย hard error และยังเขียนไฟล์ไปที่ path ที่ซ้อนอยู่ดี:
error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting mustbe explicitly set to this or another path to adjust your output's file layout.tsc --init เขียนอะไรให้ตอนนี้
หัวข้อที่มีชื่อว่า “tsc --init เขียนอะไรให้ตอนนี้”tsc --init generate ไฟล์สั้น ๆ ที่มีความเห็นชัดเจน แทนกำแพง option ที่ comment ไว้ นี่คือ output จริงของ tsc --init บน TypeScript 7.0.2 (ตัด comment ยาวบางส่วนออก แต่บรรทัด rootDir/outDir ที่ถูก comment ไว้คือของจริงเป๊ะ):
{ // Visit https://aka.ms/tsconfig to read more about this file "compilerOptions": { // File Layout // "rootDir": "./src", // "outDir": "./dist",
// Environment Settings // See also https://aka.ms/tsconfig/module "module": "nodenext", "target": "esnext", "types": [], // For nodejs: // "lib": ["esnext"], // "types": ["node"], // and npm install -D @types/node
// Other Outputs "sourceMap": true, "declaration": true, "declarationMap": true,
// Stricter Typechecking Options "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true,
// Style Options // "noImplicitReturns": true, // "noImplicitOverride": true, // "noUnusedLocals": true, // "noUnusedParameters": true, // "noFallthroughCasesInSwitch": true, // "noPropertyAccessFromIndexSignature": true,
// Recommended Options "strict": true, "jsx": "react-jsx", "verbatimModuleSyntax": true, "isolatedModules": true, "noUncheckedSideEffectImports": true, "moduleDetection": "force", "skipLibCheck": true, }}สังเกตว่า rootDir กับ outDir ถูก generate มาแบบ comment ทิ้งไว้ — tsc --init ให้เห็น shape แต่ไม่เดา layout ให้ ซึ่งก็คือ gotcha ของ rootDir ที่คุยไว้ข้างบนพอดี verbatimModuleSyntax กับ isolatedModules อธิบายอยู่ใน Build & Emit; หลายบรรทัดในนี้ซ้ำกับ default อยู่แล้ว (strict, noUncheckedSideEffectImports) — generator ตั้งใจเขียนให้ชัดเพื่อให้ไฟล์อธิบายตัวเองได้
ไฟล์ไหนอยู่ใน program บ้าง: include, exclude, files, extends
หัวข้อที่มีชื่อว่า “ไฟล์ไหนอยู่ใน program บ้าง: include, exclude, files, extends”ก่อนที่ option ไหนจะเปลี่ยนพฤติกรรมการ check tsc ต้องตัดสินใจก่อนว่า กำลัง check ไฟล์ไหนอยู่บ้าง — เป็นคำถามแยกต่างหากที่มาก่อน ควบคุมด้วยสี่ key นี้:
include— glob pattern ที่อธิบายไฟล์ของ program"src/**/*"แปลว่าทุกไฟล์ใต้srcexclude— glob pattern ที่ตัดออกจากสิ่งที่includematch ไว้ (ไม่ได้เพิ่มไฟล์กลับมาที่includeไม่เคยเจอ)node_modules,.git, และoutDirถูก exclude เป็น default อยู่แล้วแม้ไม่มี keyexcludefiles— list ไฟล์แต่ละไฟล์แบบเจาะจง ถูก include เสมอไม่ว่าinclude/excludeจะว่าไง — วิธีเดียวที่บังคับไฟล์หนึ่งเข้า program ได้extends— สืบทอดcompilerOptionsของ config อื่น (และตั้งแต่ 3.9 สืบทอดinclude/exclude/filesด้วย เว้นแต่ไฟล์ที่ extend จะ override เอง) ทำให้ monorepo share นโยบายเดียวข้ามหลาย package ได้
นี่คือโปรเจกต์เล็ก ๆ จริง — base config บวกกับตัวที่ extend มัน มี include, exclude, files, และ paths ครบ พร้อม output จริงของ tsc --showConfig (ตรวจใน scratch แล้ว):
{ "compilerOptions": { "target": "es2025", "module": "nodenext", "strict": true }}{ "extends": "./tsconfig.base.json", "compilerOptions": { "rootDir": "./src", "outDir": "./dist", "paths": { "@utils/*": ["./src/utils/*"] } }, "include": ["src/**/*"], "exclude": ["src/**/*.spec.ts"], "files": ["src/extra-entry.ts"]}tsc --showConfig resolve chain ของ extends, merge ทุก option เข้าเป็น object เดียว และแสดง list ไฟล์จริงที่ program จะได้:
{ "compilerOptions": { "module": "nodenext", "outDir": "./dist", "paths": { "@utils/*": ["./src/utils/*"] }, "rootDir": "./src", "strict": true, "target": "es2025", "moduleResolution": "nodenext", "moduleDetection": "force" }, "files": [ "./src/extra-entry.ts", "./src/extra-entry.ts", "./src/index.ts", "./src/utils/math.ts" ], "include": ["src/**/*"], "exclude": ["src/**/*.spec.ts"]}สองรายละเอียดที่ควรสังเกตใน output จริงนั้น: moduleResolution กับ moduleDetection โผล่มาทั้งที่ไม่มี config ไหนเขียนไว้เลย — tsc เติมค่าที่ module: nodenext implied ให้เอง และ src/extra-entry.ts ถูก list ไว้ สองครั้ง — ครั้งหนึ่งเพราะ files ระบุตรง ๆ อีกครั้งเพราะ glob ของ include ก็ match มันด้วย tsc ไม่ dedupe สองแหล่งนี้เข้าหากัน — ไฟล์ที่ถูกระบุทั้งสองที่จะโผล่สองครั้งใน list ที่ resolve แล้วเฉย ๆ ไม่มีอันตราย (ยัง check แค่ครั้งเดียว) แต่เป็นสัญญาณว่า entry ใน files นั้นซ้ำซ้อนกับ include ไปแล้ว
paths คือการ remap เฉพาะระดับ type — ไม่มีผลตอน runtime
หัวข้อที่มีชื่อว่า “paths คือการ remap เฉพาะระดับ type — ไม่มีผลตอน runtime”paths (ที่ใช้ข้างบน: "@utils/*": ["./src/utils/*"]) บอก checker ว่าจะหา module specifier ที่ไหน แค่นั้น มันไม่มีผลกับวิธีที่ code ของคุณถูกโหลดจริง ๆ เลย และการรัน compiled output ก็พิสูจน์เรื่องนี้ compile โปรเจกต์ข้างบน (tsc, emit จริง) แล้วรัน output ตรง ๆ ด้วย Node:
$ node dist/index.jsnode:internal/modules/cjs/loader:1421 throw err;Error: Cannot find module '@utils/math'Require stack:- .../dist/index.js code: 'MODULE_NOT_FOUND'tsc type-check import { add } from "@utils/math" ผ่านสำเร็จ — paths บอกมันตรง ๆ ว่า @utils/math อยู่ที่ไหนสำหรับการ check — แต่มัน emit specifier นั้น เหมือนเดิมทุกตัวอักษร และ Node ไม่เคยรู้จัก @utils/* เลย bundler ที่อ่าน paths entry เดียวกัน (Vite, webpack) จะเขียน specifier ใหม่ตอน bundle ซึ่งเป็นเหตุผลที่ paths “ใช้ได้เลย” ในโปรเจกต์ที่ใช้ bundler แต่พังเงียบ ๆ ในโปรเจกต์ที่มีแค่ tsc ถ้า tsc เป็น build step เดียวของคุณ ให้เลี่ยง paths สำหรับอะไรที่ ship จริง หรือไม่ก็ทำ runtime alias เอง — field imports ของ Node เองใน package.json (การ map #/* จาก Modules & Resolution) คือกลไก alias ตัวเดียวที่ทั้ง tsc และ Node เข้าใจร่วมกัน
tsc --showConfig: compiler คิดว่า config ของคุณหมายความว่าอะไร
หัวข้อที่มีชื่อว่า “tsc --showConfig: compiler คิดว่า config ของคุณหมายความว่าอะไร”--showConfig (ที่ใช้ข้างบน) คือทางที่เร็วที่สุดในการตอบ “เดี๋ยว extends resolve เป็นอะไรกันแน่” โดยไม่ต้องไล่อ่าน chain ของไฟล์เองทีละไฟล์ — มันไม่ compile อะไรเลย แค่ print config ที่ merge เสร็จแล้วซึ่ง tsc จะใช้จริง หยิบมาใช้เมื่อไหร่ก็ตามที่โปรเจกต์สืบทอดจาก base config ที่ share กัน แล้ว flag ตัวหนึ่งไม่ทำงานแบบที่คุณคาดไว้: JSON ที่ merge แล้วบอกคุณทันทีว่าค่าที่คุณคิดว่าตั้งไว้รอดจาก inheritance chain มาจริงไหม
อ่านตัวเลข performance ของ compiler เอง
หัวข้อที่มีชื่อว่า “อ่านตัวเลข performance ของ compiler เอง”สอง flag ทำให้ tsc รายงานตัวเองแทน (หรือเพิ่มเติมจาก) code ของคุณ --extendedDiagnostics print สรุปหลัง check เสร็จ — output จริงสำหรับโปรแกรมเล็ก ๆ ที่มี conditional type กับ mapped type อยู่บ้าง (ตรวจใน scratch แล้ว):
Files: 83Lines: 58450Identifiers: 49750Symbols: 59787Types: 35364Instantiations: 34403Memory used: 64885KParse time: 0.013sBind time: 0.005sCheck time: 0.107sEmit time: 0.000sTotal time: 0.125sInstantiations คือตัวเลขที่คอร์สนี้สนใจที่สุด: ทุกครั้งที่ generic type หรือ function ถูกแทน type argument จริงเข้าไป นับเป็นหนึ่ง instantiation และเป็นตัวเลขที่ระเบิดก่อนบน recursive conditional type ที่วิ่งเถลไถลนานก่อนที่ “Types” หรือ “Symbols” จะดูผิดปกติ — โปรแกรมที่อัตราส่วน Files/Types ดูปกติดี แต่ Instantiations สูงมาก คือสัญญาณจริงให้ไปหา type-level computation ที่กำลังคำนวณซ้ำสิ่งเดิมวนไปวนมา Check time แยกเฉพาะ phase การ type-check ออกจาก parse กับ emit จึงเป็นตัวเลขที่ควรเทียบก่อน/หลัง เปลี่ยน tsconfig (flag เข้มขึ้น, lib ใหม่) เพื่อดูว่ามันเสียต้นทุนจริงเท่าไหร่
--generateTrace <dir> ไปไกลกว่านั้น เขียน trace.json แบบเข้ากันได้กับ Chrome DevTools (เปิดใน chrome://tracing หรือ Perfetto UI) บวกกับไฟล์ types_N.json หนึ่งไฟล์ขึ้นไปที่ map type ID ทุกตัวที่พูดถึงใน trace กลับไปเป็น structure ของมัน trace คือ timeline ของ event — หลายร้อย entry แบบ {"ph":"X","name":"checkSourceFile","dur":...} — ที่ให้คุณเห็นว่า ไฟล์ไหน หรือ type ไหน ที่ checker ใช้เวลาไปกับมัน ไม่ใช่แค่ total ของทั้งโปรแกรมแบบที่ --extendedDiagnostics ให้ มันคือเครื่องมือที่ควรใช้ต่อเมื่อ --extendedDiagnostics บอกแล้วว่า check time สูง และคุณต้องหาว่า ตรงไหน ไม่ใช่แค่ ว่ามันสูง
useUnknownInCatchVariables ส่วนหนึ่งของ strict ตรวจสดจริง
// @expect-error TS18046function parseNumber(input: string): number { try { return JSON.parse(input); } catch (e) { console.log(e.message); return 0; }} นี่คือ useUnknownInCatchVariables จากตาราง strict ข้างล่างแบบสด: strict เปิดอยู่เสมอใน checker นี้ ดังนั้น e เป็น type unknown การอ่าน .message จากมันโดยไม่ narrow ก่อนคือ error จริง — catch (e) {} ธรรมดาใน JavaScript ไม่การันตีเลยว่า e เป็น Error ด้วยซ้ำ และ type ก็สะท้อนความจริงนั้นตรง ๆ แทนที่จะเดา
strict เปิดอะไรให้จริง ๆ
หัวข้อที่มีชื่อว่า “strict เปิดอะไรให้จริง ๆ”strict เป็น default แล้ว แต่ก็ยังควรรู้ว่ามันรวมอะไรไว้ เพราะคุณจะเจอ codebase ที่ปิดบางส่วนของมันไป:
Flag (อยู่ใน strict) | สิ่งที่จับได้ |
|---|---|
strictNullChecks | null และ undefined ไม่แอบอยู่ในทุก type อีกต่อไป — คุ้มที่สุด |
noImplicitAny | value ที่จะ fall back เป็น any กลายเป็น error ที่คุณต้องแก้ |
strictFunctionTypes | parameter ของ function ถูก check แบบ contravariant — callback ที่ไม่ปลอดภัยถูกจับ |
strictBindCallApply | bind, call และ apply ถูก type-check กับ signature ของ function |
strictPropertyInitialization | field ของ class ต้องถูก assign ใน constructor (หรือ mark เป็น optional) |
useUnknownInCatchVariables | catch (e) ให้ unknown ไม่ใช่ any บังคับให้คุณ narrow |
strictBuiltinIteratorReturn | iterator built-in return undefined ตอนจบ ไม่ใช่ any (5.6) |
noImplicitThis | this ที่จะ fall back เป็น any (เช่นใน plain function ที่ใช้เป็น method) กลายเป็น error |
alwaysStrict | parse ใน strict mode และ emit "use strict" — ปิดไม่ได้อีกแล้ว |
พระเอกคือ strictNullChecks ถ้าไม่เปิด string จะแอบรวม null และ undefined ไว้ด้วย และ compiler เตือนคุณเรื่อง runtime crash ที่พบบ่อยที่สุดใน JavaScript ไม่ได้เลย พอเปิดแล้ว null และ undefined กลายเป็น type ของตัวเองที่คุณต้องจัดการอย่างตั้งใจ
ถ้าคุณรับช่วงโปรเจกต์ที่มี "strict": false ให้มองเป็นงาน migration (ดู Migration Strategy) — เปิด flag ทีละตัว อย่าลบบรรทัดนั้นทิ้งแล้วจมอยู่กับ error
target, module และ lib
หัวข้อที่มีชื่อว่า “target, module และ lib”สาม option นี้อธิบายโลกที่ code ของคุณรันอยู่:
target— JavaScript version ที่tscemit ออกมา defaultes2025(และesnext) แปลว่า ไม่ down-level: syntax สมัยใหม่ถูกส่งผ่านไปตรง ๆ ซึ่งเป็นสิ่งที่ runtime ปัจจุบันทุกตัวต้องการ target ที่ต่ำกว่ายัง down-level syntax ใหม่ ๆ ได้ — แต่es5หายไปแล้ว (เป็น hard error ใน 7.0; ES2015 คือพื้น) และdownlevelIterationก็หายไปด้วยmodule— module format ของ output ใช้nodenextเมื่อ Node รัน output โดยตรง,esnextหรือpreserveเมื่อ bundler เป็นคนจัดการ และnode18/node20แบบ stable เมื่ออยาก pin พฤติกรรมไว้กับ Node สายใดสายหนึ่ง บทถัดไปลงลึกเรื่องนี้lib— type declaration built-in ตัวไหนที่ใช้ได้ ตอนนี้lib.domรวมสิ่งที่เคยอยู่ในdom.iterableและdom.asynciterableไว้แล้ว;es2025เพิ่มRegExp.escape, helper method ของIterator, method พีชคณิตของSetและ method upsert ของMap(getOrInsert,getOrInsertComputed) และ type ของTemporalมาพร้อม 6.0 ถ้าคุณใช้ API แล้วเจอ “does not exist” แปลว่าlibของคุณเก่าเกินไป
flag ข้าง ๆ strict ที่ควรเพิ่ม
หัวข้อที่มีชื่อว่า “flag ข้าง ๆ strict ที่ควรเพิ่ม”strict เป็นพื้น ไม่ใช่เพดาน อีกสอง flag จับ bug ที่ strict อย่างเดียวพลาด:
// noUncheckedIndexedAccess: trueconst arr = [1, 2, 3];const x = arr[10]; // type is now `number | undefined`, not `number`x.toFixed(); // ❌ Object is possibly 'undefined' — exactly the bug you want caughtnoUncheckedIndexedAccess— การ index array หรือ record อาจ returnundefinedได้ (index 10 ของ array 3 ตัวก็ return) type จึงสะท้อนความจริงนั้น มีประสิทธิภาพมากในการกัน bug out-of-boundsexactOptionalPropertyTypes— แยกความต่างระหว่าง property ที่ set เป็นundefinedกับ property ที่หายไปเลย ทำให้ optionalname?: stringไม่แอบรับname: undefinedอีกต่อไปnoUncheckedSideEffectImports(default ตั้งแต่ 6.0) — เมื่อก่อนimport "./styles.css"ที่ path ผิดจะ resolve เป็นความว่างเปล่าเงียบ ๆ ตอนนี้พิมพ์ผิดคือ error ประกาศdeclare module "*.css" {}ครั้งเดียวสำหรับ asset import ที่ bundler จัดการให้erasableSyntaxOnly(5.8) — ห้าม syntax ของ TypeScript ที่ต้อง transform ไม่ใช่แค่ ลบ:enum,namespaceที่มี runtime code, parameter property ใน class,import x = require()เปิดไว้ถ้าคุณรันไฟล์.tsตรง ๆ ใน Node (type stripping), Deno หรือ Bun ที่ runtime แค่ strip type ออก
option ที่ถูกถอดออก และอะไรมาแทน
หัวข้อที่มีชื่อว่า “option ที่ถูกถอดออก และอะไรมาแทน”TypeScript 6.0 deprecate option ชุดหนึ่ง และ 7.0 ถอดออกไปเลย ใน 6.x ซื้อเวลาได้ด้วย "ignoreDeprecations": "6.0" แต่ทุกตัวมีของแทนตรง ๆ:
| ถูกถอด | แทนด้วย |
|---|---|
baseUrl | ยุบ prefix เข้าไปใน paths: "@app/*": ["./src/app/*"] |
moduleResolution: node / node10 / classic | nodenext (Node รัน) หรือ bundler (bundler จัดการ) |
module: amd / umd / system / none | esnext หรือ preserve แล้วให้ bundler ผลิต format อื่น |
outFile | bundler |
target: es5, downlevelIteration | es2015 หรือใหม่กว่า |
esModuleInterop: false, allowSyntheticDefaultImports: false, alwaysStrict: false | ไม่ต้องทำอะไร — เปิดตลอดเวลาแล้ว |
import x from "./data.json" asserts { type: "json" } | with { type: "json" } |
module Foo {} (keyword namespace แบบเก่า) | namespace Foo {} |
/// <reference no-default-lib="true" /> | noLib หรือ libReplacement |
มีการเปลี่ยน CLI อีกเรื่องที่อยู่ตรงนี้ด้วย: การรัน tsc file.ts ใน directory ที่มี tsconfig.json ตอนนี้เป็น error (TS5112) เพราะมันแอบ ignore config มาหลายปี ใส่ --ignoreConfig ถ้าตั้งใจแบบนั้นจริง ๆ
option เล็ก ๆ ที่คุณจะได้ใช้จริง
หัวข้อที่มีชื่อว่า “option เล็ก ๆ ที่คุณจะได้ใช้จริง”${configDir}(5.5) — template variable สำหรับ path ใน base config ที่ share กัน ทำให้"outDir": "${configDir}/dist"resolve เทียบกับโปรเจกต์ที่ extend ไม่ใช่ไฟล์ basenoCheck(5.6) — emit โดยไม่ type-check เป็นภาพสะท้อนของnoEmitมีประโยชน์เมื่อ step อื่นเป็นคน checkisolatedDeclarations(5.5) — บังคับ type ชัดเจนบน export เพื่อให้ผลิต.d.tsทีละไฟล์แบบขนานได้โดยไม่ต้องใช้ checkerrewriteRelativeImportExtensions(5.7) — ให้คุณเขียนimport "./util.ts"แล้วtscemit เป็น./util.jslibReplacement(5.8) — opt in ให้แทนlib.domด้วย npm package เช่น@typescript/lib-dom; ปิดเป็น default ตั้งแต่ 6.0skipLibCheck— ข้ามการ check ไฟล์.d.tsในnode_modulesยังเป็นการเร่งความเร็วที่ถูกที่สุดตัวเดียว แม้บน native compiler
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”tsconfig.json ตอบสองคำถามที่เอาไปปนกันง่ายแต่จริง ๆ อิสระต่อกันโดยสิ้นเชิง: ไฟล์ไหนอยู่ใน program (include/exclude/files/extends) กับ มัน check เข้มแค่ไหน (strict และครอบครัวของมัน) --showConfig มีไว้เพราะทั้งสองคำถามตอบยากขึ้นเมื่อ extends เข้ามาเกี่ยวข้อง — มันคือ compiler บอกความจริงเกี่ยวกับ state ที่ merge แล้วของตัวเอง แทนที่คุณจะต้องไล่ inheritance chain ด้วยตา และ paths เป็นลูกพี่ลูกน้องของคำถามแรกในเรื่อง module resolution ซึ่งเป็นเรื่องของ checker เท่านั้น: มันเปลี่ยนสิ่งที่ tsc เชื่อว่า specifier ชี้ไปที่ไหน ไม่เคยเปลี่ยนสิ่งที่ runtime loader ทำกับ specifier ตัวเดียวกันนั้นเลย — การพิสูจน์ช่องว่างนี้ (Cannot find module จริงหลัง compile สะอาด) คือทางที่เร็วที่สุดที่จะทำให้เข้าใจว่า tsconfig คือคำสั่งให้ checker ไม่ใช่ให้ JavaScript ที่รันจริงในที่สุด
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
glob include/exclude | ขยายเองอัตโนมัติตามไฟล์ที่เพิ่มเข้ามา | ง่ายที่จะ include สิ่งที่ไม่ได้ตั้งใจ (.spec.ts ที่ไม่มี exclude entry) |
files (list ตรง ๆ) | เจาะจง ไม่มีการ include เกินคาด | ไม่ scale — ไฟล์ใหม่ทุกไฟล์ต้องแก้เองด้วยมือ |
extends จาก base config ที่ share กัน | นโยบาย strictness/target เดียวทั้ง monorepo | flag ที่คุณไม่คาดคิดอาจถูกสืบทอดมาเงียบ ๆ — ควร sanity-check ด้วย --showConfig เสมอ |
paths สำหรับ alias ภายใน | import สะอาดขึ้น (@utils/x แทน ../../../utils/x) | เฉพาะระดับ type — build ที่มีแค่ tsc จะ ship import ที่พังเงียบ ๆ เว้นแต่มีอะไรอื่น (bundler, field imports ของ Node เอง) เขียน specifier ใหม่ด้วย |
--extendedDiagnostics / --generateTrace | ตัวเลขจริงแทนความรู้สึก “build ช้าเหมือนจะ” | output ของ --generateTrace ใหญ่และต้องมี trace viewer ถึงอ่านได้; --extendedDiagnostics เฉย ๆ ไม่บอกว่าไฟล์หรือ type ไหนแพง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”pathsคือ module alias จริง เหมือนของ bundler มันบอกแค่ checker ของtscว่า specifier resolve ไปที่ไหนสำหรับ type-checking — พิสูจน์แล้วข้างบน:tscbuild ที่สะอาดพร้อมpathsยัง emit specifier ที่ไม่ resolve ออกมาเหมือนเดิม และ Node throwCannot find moduleตอน runtime เว้นแต่ bundler หรือ runtime alias mechanism จริง ๆ จะเข้าใจมันด้วยextendsสืบทอดแค่compilerOptionsตั้งแต่ 3.9 มันสืบทอดinclude/exclude/filesจาก base config ด้วย เว้นแต่ไฟล์ที่ extend จะตั้งของตัวเอง — config ที่ดูเหมือนตั้งแค่ compiler flag ไม่กี่ตัวอาจแอบพา file list ของ base มาด้วย- ไฟล์ที่ถูก list ทั้งใน
filesและถูกincludematch ด้วย คือ error หรือถูก dedupe ไม่ทั้งคู่ — output จริงของtsc --showConfigข้างบน list มันสองครั้ง ไม่มีอันตราย แค่เป็นสัญญาณว่า entry ในfilesซ้ำซ้อน - ตัวเลข
Typesของ--extendedDiagnosticsคือตัวที่ควรจับตาสำหรับ build ที่ช้าInstantiationsมักเป็นตัวเลขที่ actionable กว่า — มันระเบิดเมื่อ generic หรือ recursive conditional type คำนวณสิ่งเดิมซ้ำ ๆ ในขณะที่Typesยังดูปกติอยู่
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”tsconfig.jsonที่ generate มาตั้งpathsแล้วสมมติว่าแค่นั้นพอให้ import ทำงานตอน runtime กับtscbuild ธรรมดาไหม? ตรวจแบบที่บทนี้ทำ: compile จริง รัน output ตรง ๆ แล้วดูว่าCannot find moduleโผล่มาไหม- config
extendsจาก base ที่ share กันโดยไม่ตรวจว่า base นั้นตั้งอะไรไว้จริง ๆ ไหม? รันtsc --showConfigกับผลที่ merge แล้วก่อนเชื่อว่า flag หนึ่งเปิด (หรือปิด) อยู่ — คำตอบอยู่แค่คำสั่งเดียว ไม่ต้องเดาจากการอ่านสองไฟล์ด้วยตา excludeถูกใช้ในที่ที่ scope ด้วยfiles/includeแม่นกว่าไหม — เช่น exclude**/*.spec.tsแทนที่จะไม่ include ไฟล์ test เข้า config หลักตั้งแต่แรก? ทั้งสองแบบใช้ได้ แต่excludeที่กว้างเกินไปเสี่ยงถูกขยายโดยไม่ตั้งใจทีหลังเมื่อมีไฟล์ใหม่ที่ match pattern ที่ exclude ไว้แค่ชื่อ- tsconfig ที่ generate มาก็อปจาก tutorial ที่ยังตั้ง
"moduleResolution": "node"หรือ"baseUrl"— ทั้งคู่ถูกถอดใน TypeScript 7 (hard error ไม่ใช่ no-op เงียบ ๆ) — เป็นสัญญาณว่าการ generate ไม่ได้ตรวจกับ TypeScript version จริงของโปรเจกต์นี้
🐛 หา bug ในโค้ดนี้
{"extends": "../../tsconfig.base.json","compilerOptions": { "outDir": "./dist", "paths": { "@shared/*": ["../shared/src/*"] }},"include": ["src/**/*"]}เฉลย
มีปัญหาสองจุด ทั้งคู่มองไม่เห็นถ้าอ่านไฟล์นี้เดี่ยว ๆ อย่างแรกไม่มี "rootDir" เลย — พอตั้ง outDir ไว้แต่ปล่อย rootDir เป็น default นี่คือ gotcha เป๊ะจากก่อนหน้านี้ในบทนี้: tsc หยุดด้วย hard error TS5011 จริง ๆ และยังเขียนไฟล์ไปที่ path ที่ซ้อนอยู่ดี ไม่ใช่ความสำเร็จเงียบ ๆ อย่างที่สอง paths map @shared/* เข้าไปใน src ของ package ข้าง ๆ — compile ผ่านสบาย (checker เห็นไฟล์ได้) แต่ไม่มีอะไรเขียน @shared/* ใหม่ตอน emit ดังนั้นเว้นแต่มี bundler อยู่ใน build นี้ด้วย (และอ่าน paths entry เดียวกัน) compiled output จะ throw Cannot find module '@shared/utils' ทันทีที่รัน ตรงตามที่บทนี้พิสูจน์ด้วยการรัน node dist/index.js จริง การ review tsconfig ที่ generate มาต้องตรวจทั้งสองอย่าง: rootDir ต้องตั้งชัดไหมเมื่อมี outDir แบบนี้ และมีอะไรนอกจาก tsc เองที่ resolve ทุก paths entry ตอน runtime จริงไหม