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

tsconfig, Deep

ตลอดสิบปี 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.0default ใน 6.0 / 7.0
strictfalsetrue
modulecommonjsesnext
targetประมาณ es2020es2025 — ขยับตาม ECMAScript ของปีปัจจุบัน
types@types/* ทุกตัวใน node_modules[] — ไม่โหลดอะไรเลยนอกจากที่คุณระบุ
rootDirinfer จาก source ของคุณ. — directory ของ tsconfig.json
noUncheckedSideEffectImportsfalsetrue
libReplacementtruefalse

สองตัวในนี้กัดโปรเจกต์จริงตอน 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 must
be explicitly set to this or another path to adjust your output's file layout.

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 ตั้งใจเขียนให้ชัดเพื่อให้ไฟล์อธิบายตัวเองได้

ก่อนที่ option ไหนจะเปลี่ยนพฤติกรรมการ check tsc ต้องตัดสินใจก่อนว่า กำลัง check ไฟล์ไหนอยู่บ้าง — เป็นคำถามแยกต่างหากที่มาก่อน ควบคุมด้วยสี่ key นี้:

  • include — glob pattern ที่อธิบายไฟล์ของ program "src/**/*" แปลว่าทุกไฟล์ใต้ src
  • exclude — glob pattern ที่ตัดออกจากสิ่งที่ include match ไว้ (ไม่ได้เพิ่มไฟล์กลับมาที่ include ไม่เคยเจอ) node_modules, .git, และ outDir ถูก exclude เป็น default อยู่แล้วแม้ไม่มี key exclude
  • files — 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 แล้ว):

tsconfig.base.json
{
"compilerOptions": {
"target": "es2025",
"module": "nodenext",
"strict": true
}
}
tsconfig.json
{
"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 (ที่ใช้ข้างบน: "@utils/*": ["./src/utils/*"]) บอก checker ว่าจะหา module specifier ที่ไหน แค่นั้น มันไม่มีผลกับวิธีที่ code ของคุณถูกโหลดจริง ๆ เลย และการรัน compiled output ก็พิสูจน์เรื่องนี้ compile โปรเจกต์ข้างบน (tsc, emit จริง) แล้วรัน output ตรง ๆ ด้วย Node:

$ node dist/index.js
node: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 เข้าใจร่วมกัน

--showConfig (ที่ใช้ข้างบน) คือทางที่เร็วที่สุดในการตอบ “เดี๋ยว extends resolve เป็นอะไรกันแน่” โดยไม่ต้องไล่อ่าน chain ของไฟล์เองทีละไฟล์ — มันไม่ compile อะไรเลย แค่ print config ที่ merge เสร็จแล้วซึ่ง tsc จะใช้จริง หยิบมาใช้เมื่อไหร่ก็ตามที่โปรเจกต์สืบทอดจาก base config ที่ share กัน แล้ว flag ตัวหนึ่งไม่ทำงานแบบที่คุณคาดไว้: JSON ที่ merge แล้วบอกคุณทันทีว่าค่าที่คุณคิดว่าตั้งไว้รอดจาก inheritance chain มาจริงไหม

สอง flag ทำให้ tsc รายงานตัวเองแทน (หรือเพิ่มเติมจาก) code ของคุณ --extendedDiagnostics print สรุปหลัง check เสร็จ — output จริงสำหรับโปรแกรมเล็ก ๆ ที่มี conditional type กับ mapped type อยู่บ้าง (ตรวจใน scratch แล้ว):

Files: 83
Lines: 58450
Identifiers: 49750
Symbols: 59787
Types: 35364
Instantiations: 34403
Memory used: 64885K
Parse time: 0.013s
Bind time: 0.005s
Check time: 0.107s
Emit time: 0.000s
Total time: 0.125s

Instantiations คือตัวเลขที่คอร์สนี้สนใจที่สุด: ทุกครั้งที่ 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 TS18046
function parseNumber(input: string): number {
try {
return JSON.parse(input);
} catch (e) {
console.log(e.message);
return 0;
}
}
Open in TS Playground

นี่คือ useUnknownInCatchVariables จากตาราง strict ข้างล่างแบบสด: strict เปิดอยู่เสมอใน checker นี้ ดังนั้น e เป็น type unknown การอ่าน .message จากมันโดยไม่ narrow ก่อนคือ error จริง — catch (e) {} ธรรมดาใน JavaScript ไม่การันตีเลยว่า e เป็น Error ด้วยซ้ำ และ type ก็สะท้อนความจริงนั้นตรง ๆ แทนที่จะเดา

strict เป็น default แล้ว แต่ก็ยังควรรู้ว่ามันรวมอะไรไว้ เพราะคุณจะเจอ codebase ที่ปิดบางส่วนของมันไป:

Flag (อยู่ใน strict)สิ่งที่จับได้
strictNullChecksnull และ undefined ไม่แอบอยู่ในทุก type อีกต่อไป — คุ้มที่สุด
noImplicitAnyvalue ที่จะ fall back เป็น any กลายเป็น error ที่คุณต้องแก้
strictFunctionTypesparameter ของ function ถูก check แบบ contravariant — callback ที่ไม่ปลอดภัยถูกจับ
strictBindCallApplybind, call และ apply ถูก type-check กับ signature ของ function
strictPropertyInitializationfield ของ class ต้องถูก assign ใน constructor (หรือ mark เป็น optional)
useUnknownInCatchVariablescatch (e) ให้ unknown ไม่ใช่ any บังคับให้คุณ narrow
strictBuiltinIteratorReturniterator built-in return undefined ตอนจบ ไม่ใช่ any (5.6)
noImplicitThisthis ที่จะ fall back เป็น any (เช่นใน plain function ที่ใช้เป็น method) กลายเป็น error
alwaysStrictparse ใน strict mode และ emit "use strict" — ปิดไม่ได้อีกแล้ว

พระเอกคือ strictNullChecks ถ้าไม่เปิด string จะแอบรวม null และ undefined ไว้ด้วย และ compiler เตือนคุณเรื่อง runtime crash ที่พบบ่อยที่สุดใน JavaScript ไม่ได้เลย พอเปิดแล้ว null และ undefined กลายเป็น type ของตัวเองที่คุณต้องจัดการอย่างตั้งใจ

ถ้าคุณรับช่วงโปรเจกต์ที่มี "strict": false ให้มองเป็นงาน migration (ดู Migration Strategy) — เปิด flag ทีละตัว อย่าลบบรรทัดนั้นทิ้งแล้วจมอยู่กับ error

สาม option นี้อธิบายโลกที่ code ของคุณรันอยู่:

  • target — JavaScript version ที่ tsc emit ออกมา default es2025 (และ 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 ของคุณเก่าเกินไป

strict เป็นพื้น ไม่ใช่เพดาน อีกสอง flag จับ bug ที่ strict อย่างเดียวพลาด:

// noUncheckedIndexedAccess: true
const 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 caught
  • noUncheckedIndexedAccess — การ index array หรือ record อาจ return undefined ได้ (index 10 ของ array 3 ตัวก็ return) type จึงสะท้อนความจริงนั้น มีประสิทธิภาพมากในการกัน bug out-of-bounds
  • exactOptionalPropertyTypes — แยกความต่างระหว่าง property ที่ set เป็น undefined กับ property ที่หายไปเลย ทำให้ optional name?: 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 ออก

TypeScript 6.0 deprecate option ชุดหนึ่ง และ 7.0 ถอดออกไปเลย ใน 6.x ซื้อเวลาได้ด้วย "ignoreDeprecations": "6.0" แต่ทุกตัวมีของแทนตรง ๆ:

ถูกถอดแทนด้วย
baseUrlยุบ prefix เข้าไปใน paths: "@app/*": ["./src/app/*"]
moduleResolution: node / node10 / classicnodenext (Node รัน) หรือ bundler (bundler จัดการ)
module: amd / umd / system / noneesnext หรือ preserve แล้วให้ bundler ผลิต format อื่น
outFilebundler
target: es5, downlevelIterationes2015 หรือใหม่กว่า
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 ถ้าตั้งใจแบบนั้นจริง ๆ

  • ${configDir} (5.5) — template variable สำหรับ path ใน base config ที่ share กัน ทำให้ "outDir": "${configDir}/dist" resolve เทียบกับโปรเจกต์ที่ extend ไม่ใช่ไฟล์ base
  • noCheck (5.6) — emit โดยไม่ type-check เป็นภาพสะท้อนของ noEmit มีประโยชน์เมื่อ step อื่นเป็นคน check
  • isolatedDeclarations (5.5) — บังคับ type ชัดเจนบน export เพื่อให้ผลิต .d.ts ทีละไฟล์แบบขนานได้โดยไม่ต้องใช้ checker
  • rewriteRelativeImportExtensions (5.7) — ให้คุณเขียน import "./util.ts" แล้ว tsc emit เป็น ./util.js
  • libReplacement (5.8) — opt in ให้แทน lib.dom ด้วย npm package เช่น @typescript/lib-dom; ปิดเป็น default ตั้งแต่ 6.0
  • skipLibCheck — ข้ามการ check ไฟล์ .d.ts ใน node_modules ยังเป็นการเร่งความเร็วที่ถูกที่สุดตัวเดียว แม้บน native compiler

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 เดียวทั้ง monorepoflag ที่คุณไม่คาดคิดอาจถูกสืบทอดมาเงียบ ๆ — ควร 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 — พิสูจน์แล้วข้างบน: tsc build ที่สะอาดพร้อม paths ยัง emit specifier ที่ไม่ resolve ออกมาเหมือนเดิม และ Node throw Cannot 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 และถูก include match ด้วย คือ error หรือถูก dedupe ไม่ทั้งคู่ — output จริงของ tsc --showConfig ข้างบน list มันสองครั้ง ไม่มีอันตราย แค่เป็นสัญญาณว่า entry ใน files ซ้ำซ้อน
  • ตัวเลข Types ของ --extendedDiagnostics คือตัวที่ควรจับตาสำหรับ build ที่ช้า Instantiations มักเป็นตัวเลขที่ actionable กว่า — มันระเบิดเมื่อ generic หรือ recursive conditional type คำนวณสิ่งเดิมซ้ำ ๆ ในขณะที่ Types ยังดูปกติอยู่
  • tsconfig.json ที่ generate มาตั้ง paths แล้วสมมติว่าแค่นั้นพอให้ import ทำงานตอน runtime กับ tsc build ธรรมดาไหม? ตรวจแบบที่บทนี้ทำ: 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 จริงไหม

ตั้งแต่ TypeScript 6.0 ค่า default ของ strict คืออะไร?
strictNullChecks เปลี่ยนอะไร?
เมื่อเปิด noUncheckedIndexedAccess แล้ว type ของ arr[i] สำหรับ const arr: number[] คืออะไร?
หลัง upgrade เป็น TypeScript 6/7 มี @types/node install อยู่แต่หา global ของ Node ไม่เจอ เพราะอะไร?
tsconfig ของคุณมี "baseUrl": "./src" ของแทนใน TypeScript 7 คืออะไร?
หลัง build tsc แบบสะอาด รัน node dist/index.js (ที่ import จาก @utils/math) throw Cannot find module '@utils/math' ทำไม ทั้งที่ tsc compile ผ่านไม่มี error เลย?
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "paths": { "@utils/*": ["./src/utils/*"] }
  },
  "include": ["src/**/*"],
  "files": ["src/extra-entry.ts"]
}
ตัวเลขไหนของ --extendedDiagnostics มีประโยชน์ที่สุดในการจับ generic หรือ recursive conditional type ที่วิ่งเถลไถล?
Files:              83
Instantiations:  34403
Check time:      0.107s