โหมดมืด
บทที่ 09 — Module และ Declaration File
⏱ ใช้เวลา 2–3 ชั่วโมง
บทนี้เรียนอะไร
จนถึงตอนนี้เราเขียนโค้ดในไฟล์เดียว แต่โปรเจกต์จริงมีหลายไฟล์ — บทนี้เรียนการ "แบ่งโค้ดเป็นไฟล์ ๆ และเชื่อมต่อกัน" รวมถึงเรื่องสำคัญ — declaration file (.d.ts) ที่เป็นวิธีให้ TypeScript "รู้จัก" โค้ดที่ไม่ได้เขียนด้วย TypeScript
หัวข้อหลัก:
- ระบบ module —
import/export - การ import เฉพาะ type (
import type) - Declaration file คืออะไร และทำงานยังไง
- การบอก type ให้กับ "ของที่ TypeScript ไม่รู้จัก" (ambient declaration)
- การเพิ่ม/ขยาย type ของ library ที่มีอยู่
@types/*packages
⚠️ ต้องอ่านก่อน: บทนี้สมมติว่าคุณเข้าใจ ES Module (
import/export) และ CommonJS (require/module.exports) จากฝั่ง JavaScript แล้ว — ถ้ายังไม่แม่น อ่าน JavaScript บทที่ 10 — Modules ก่อน แล้วค่อยกลับมา — บทนี้จะเน้น "ส่วนที่ TypeScript เพิ่มเข้ามา"
Part 1: ทบทวน Module — import / export
1.1 แนวคิด
"Module" คือ "ไฟล์หนึ่งไฟล์" — แต่ละไฟล์มีขอบเขตของตัวเอง ตัวแปร/ฟังก์ชันในไฟล์หนึ่งจะ "ซ่อน" จากไฟล์อื่น เว้นแต่ เราจงใจ export ออกมา และไฟล์อื่น import เข้าไป
typescript
// ไฟล์: math.ts
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}typescript
// ไฟล์: main.ts
import { PI, add } from "./math";
console.log(add(PI, 1)); // ใช้ของจาก math.ts ได้export— "เปิดเผย" สิ่งนี้ให้ไฟล์อื่นใช้ได้import { ... } from "..."— "ดึง" สิ่งที่ไฟล์อื่น export มาใช้
1.2 Default Export
แต่ละไฟล์มี "export ตัวหลัก" ได้หนึ่งตัว ด้วย export default:
typescript
// ไฟล์: Calculator.ts
export default class Calculator {
add(a: number, b: number) { return a + b; }
}typescript
// ไฟล์: main.ts
import Calculator from "./Calculator"; // default import — ไม่มีปีกกาสังเกต — default import ไม่มีวงเล็บปีกกา {} และเราตั้งชื่ออะไรก็ได้ตอน import
💡 คำแนะนำ: ในงานสมัยใหม่ คนนิยมใช้ named export (
export { ... }) มากกว่า default export เพราะชื่อจะคงที่ ทำให้ rename และ autocomplete แม่นยำกว่า
Part 2: Type-only Import — import เฉพาะ type
2.1 ปัญหาที่ต้องเข้าใจก่อน
จำ type erasure จากบทที่ 00 ได้ไหม — type หายไปตอน compile แต่ "ค่า" (value) ยังอยู่ เวลาเรา import บางครั้งเรา import "type" บางครั้ง import "ค่า":
typescript
import { User, createUser } from "./user";
// ↑ type ↑ function (ค่า)User เป็น type (จะหายตอน compile) ส่วน createUser เป็นฟังก์ชัน (เป็นค่าจริง อยู่ถึง runtime)
ปัญหา: เครื่องมือ build บางตัวดูแค่ "ข้อความ" ของ import มันแยกไม่ออกว่าอันไหน type อันไหนค่า ทำให้บางครั้ง import type ที่ควรหายไป กลับถูกทิ้งไว้ในไฟล์ผลลัพธ์
2.2 ทางออก — import type
เราบอก TypeScript ให้ชัดว่า "อันนี้เป็น type นะ" ด้วย import type:
typescript
// บอกชัดว่า import นี้เป็น type ทั้งหมด — จะถูกลบทิ้งตอน compile แน่นอน
import type { User, Product } from "./types";
// import แบบผสม — ใส่ type กำกับเฉพาะตัวที่เป็น type
import { createUser, type User } from "./user";ในบทที่ 01 เราเปิด verbatimModuleSyntax ไว้ — ตัวเลือกนี้จะ บังคับ ให้เราเขียน type กำกับเมื่อ import type ทำให้โค้ดชัดเจนและ build ถูกต้องเสมอ
🎯 กฎง่าย ๆ: เมื่อ import สิ่งที่เป็น type อย่างเดียว (type, interface) ใส่
import typeหรือtypeกำกับเสมอ — IDE มักเตือนและแก้ให้อัตโนมัติได้
Part 3: Declaration File (.d.ts) — ไฟล์ประกาศชนิด (ไฟล์ที่มีแต่ type)
📎 คำศัพท์: declaration file = "ไฟล์ประกาศชนิด" — ไฟล์ที่มีแต่ข้อมูล type ไม่มีโค้ดทำงานจริง
(อีกสองคำ — ambient declaration และ module augmentation — จะแนะนำทีละคำตอนที่เจอในเนื้อหาข้างล่าง ไม่ต้องจำตอนนี้)
3.1 แนวคิด — ปัญหาที่ต้องแก้
ลองนึกสถานการณ์นี้ — คุณติดตั้ง library เก่า ๆ ที่เขียนด้วย JavaScript ล้วน เช่น:
typescript
import { doSomething } from "old-js-library";
doSomething("hello");
// ↑ TypeScript ไม่รู้เลยว่า doSomething รับอะไร คืนอะไรlibrary นี้เขียนด้วย JavaScript — ไม่มีข้อมูล type TypeScript จึงมองมันเป็น any ทั้งหมด เราเสียประโยชน์ของ TypeScript ไป
ทางแก้คือ declaration file — ไฟล์นามสกุล .d.ts ที่ "บรรยาย type" ของโค้ด JavaScript นั้น โดยไม่มีโค้ดทำงานจริง
3.2 หน้าตาของ .d.ts
typescript
// ไฟล์: old-js-library.d.ts
// ไฟล์นี้มีแต่ "คำบรรยาย type" ไม่มี implementation จริง
export declare function doSomething(input: string): number;
export declare const VERSION: string;สังเกตคำว่า declare — มันบอกว่า "สิ่งนี้มีอยู่จริง (ที่ไหนสักแห่ง) ฉันแค่มาบอก type ของมัน อย่าคาดหวังโค้ดทำงานจากไฟล์นี้"
เมื่อมีไฟล์นี้ TypeScript จะรู้จัก doSomething ทันที — autocomplete ทำงาน, ใส่ argument ผิดก็ฟ้อง
3.3 declaration file มาจากไหนได้บ้าง
- library สมัยใหม่ — มีมาให้ในตัว — library ที่เขียนด้วย TypeScript (เช่น React, Zod) จะ ship ไฟล์
.d.tsมาด้วย คุณไม่ต้องทำอะไร @types/*— ชุมชนช่วยเขียน — สำหรับ library JavaScript ยอดนิยม (จะพูดถึงใน Part 6)- เขียนเอง — สำหรับ library ที่ไม่มีทั้งสองอย่าง หรือสำหรับบรรยาย "ของพิเศษ" ในโปรเจกต์เรา
3.4 สร้าง .d.ts อัตโนมัติ (สำหรับคนเขียน library)
ถ้าคุณเขียน library ด้วย TypeScript เอง — เวลาเผยแพร่ คุณต้อง ship .d.ts ให้คนใช้ TypeScript สร้างให้อัตโนมัติด้วย:
bash
tsc --declaration # สร้างทั้ง .js และ .d.ts
tsc --declaration --emitDeclarationOnly # สร้างแค่ .d.ts (ให้ bundler ทำ .js)แล้วบอกใน package.json ว่าไฟล์ type อยู่ที่ไหน:
json
{
"types": "./dist/index.d.ts"
}Part 4: Ambient Declaration — บอก type ให้ "ของที่ลอยมาจากไหนไม่รู้"
📎 คำศัพท์: ambient declaration = "การประกาศแบบลอย" — บอก type ของของที่ "ลอยอยู่ในสภาพแวดล้อม" runtime (มีอยู่ที่ไหนสักแห่งจริง ๆ แต่ TypeScript ยังไม่รู้จัก)
"Ambient" แปลว่า "ที่อยู่รอบ ๆ" — ในบริบทนี้หมายถึง "ของที่มีอยู่ในสภาพแวดล้อม runtime แต่ TypeScript ไม่รู้จัก"
4.1 ตัวอย่าง — global variable ที่ถูกฉีดเข้ามา
บางครั้งมี <script> ภายนอกฉีดตัวแปร global เข้ามา (เช่น Google Analytics ฉีด gtag) TypeScript ไม่รู้จัก เราบอกมันได้:
typescript
// ไฟล์: globals.d.ts
declare const APP_VERSION: string;
declare function gtag(command: string, ...args: unknown[]): void;หลังจากนี้ TypeScript จะรู้จัก APP_VERSION และ gtag — ใช้ได้โดยไม่ error
4.2 บอก type ให้ไฟล์ที่ไม่ใช่โค้ด
เวลาทำเว็บ เรามักต้อง import ไฟล์แปลก ๆ ที่ไม่ใช่ .ts เช่นรูปภาพหรือ CSS:
typescript
import logo from "./logo.svg"; // ❌ TypeScript: ไม่รู้จักไฟล์ .svgเราบอก TypeScript ว่า "ไฟล์ .svg ทุกไฟล์ ให้ถือว่า import มาเป็น string (URL)":
typescript
// ไฟล์: assets.d.ts
declare module "*.svg" {
const url: string;
export default url;
}
declare module "*.css" {
const classes: { readonly [key: string]: string };
export default classes;
}หลังจากนี้ import logo from "./logo.svg" จะใช้ได้ และ logo มี type เป็น string
💡 ถ้าใช้ Vite อยู่แล้ว: ส่วนใหญ่ไม่ต้องเขียน
declare module "*.svg"เอง — Vite มีvite/client.d.tsมาให้ และเราเพิ่มบรรทัด/// <reference types="vite/client" />หรือใส่"types": ["vite/client"]ในtsconfig.jsonก็พอ ถ้าเขียนเองซ้ำซ้อนกัน type อาจชน — ตรวจก่อนว่ามีอยู่แล้วหรือยัง
4.3 declare global — เพิ่ม property ให้ของ global
บางครั้งเราอยากเพิ่ม property ให้ window (object global ของเบราว์เซอร์):
typescript
declare global {
interface Window {
myAppConfig: { apiUrl: string };
}
}
// หลังจากนี้ใช้ได้:
window.myAppConfig.apiUrl; // ✅ TypeScript รู้จักdeclare global ใช้เทคนิค "declaration merging" ของ interface (จากบทที่ 04) — มัน "รวม" property ใหม่เข้ากับ Window ที่ TypeScript มีอยู่แล้ว
Part 5: Module Augmentation — ขยาย type ของ library ที่มีอยู่
บางครั้ง library มี type มาให้แล้ว แต่เราอยาก "เพิ่ม" บางอย่างเข้าไป ทำได้ด้วย module augmentation:
📎 คำศัพท์: module augmentation = "การขยาย module" — เพิ่ม/ขยาย type ของ library ที่มีอยู่แล้ว (ไม่ใช่สร้าง type ใหม่ทั้งหมด)
📎 คำศัพท์: middleware (มิดเดิลแวร์) = ฟังก์ชันที่คั่นกลางกระบวนการจัดการ request ก่อนถึงโค้ดหลัก — มักใช้เติม/ตรวจสอบข้อมูลระหว่างทาง (เช่น เช็ก token แล้วฝัง
userลงใน request)
typescript
// สมมติ library "express" มี interface Request อยู่แล้ว
// เราอยากเพิ่ม property "user" เข้าไป (เพราะ middleware ของเราใส่มัน)
import "express";
declare module "express" {
interface Request {
user?: { id: number; name: string };
}
}
// หลังจากนี้ ในโค้ดที่ใช้ express:
// req.user → TypeScript รู้จักแล้วไวยากรณ์สำคัญ:
import "express"บรรทัดบนสุด — บังคับให้ไฟล์นี้เป็น "module" (มี import/export อย่างน้อยหนึ่งอย่าง) ซึ่งจำเป็นสำหรับdeclare moduleของ library💡 การเขียน
import "ชื่อ";โดยไม่ดึงอะไรออกมาเลย (ไม่มี{ ... }หรือชื่อตัวแปร) เรียกว่า side-effect import — เป็นรูปแบบ import ที่ถูกต้องตามปกติ ไม่ใช่ syntax พิเศษ มันแค่ "บอกว่าไฟล์นี้ใช้ module นี้" โดยไม่ต้องนำค่าอะไรมาใช้จริง คนละแบบกับimport Calculator from "./Calculator"ที่ดึงค่ามาใช้declare module "ชื่อ-library"ตามด้วยส่วนที่จะ "รวมเข้า" type เดิม — อาศัย declaration merging (การรวมการประกาศ) ของ TypeScript เพื่อรวมinterface Requestใหม่กับของเดิม
declare module "ชื่อ-library" คือการ "เปิด" type ของ library นั้นเข้ามา แล้วเพิ่มของเข้าไป — TypeScript จะรวมเข้ากับ type เดิมของ library
Part 6: @types/* — declaration file จากชุมชน
6.1 ทำไมต้องมี @types/*
library JavaScript ยอดนิยมหลายตัว (เช่น lodash, express รุ่นเก่า) เขียนด้วย JavaScript ล้วน ไม่มี .d.ts มาให้ — แต่มีคนใช้เยอะมาก
ชุมชนนักพัฒนาจึงช่วยกันเขียน declaration file ให้ library พวกนี้ แล้วเผยแพร่ในชื่อ @types/ชื่อ-library:
bash
npm install lodash # ตัว library จริง (JavaScript)
npm install -D @types/lodash # declaration file ที่ชุมชนเขียนหลังติดตั้งทั้งคู่ — import _ from "lodash" จะมี type ครบ TypeScript รู้จัก function ทุกตัว
6.2 รู้ได้ยังไงว่าต้องลง @types
ง่ายมาก — ถ้า import library แล้ว TypeScript ฟ้องประมาณว่า "Could not find a declaration file for module ..." — แปลว่าต้องลง @types/ชื่อนั้น
แต่ library สมัยใหม่ส่วนใหญ่ (ที่เขียนด้วย TypeScript) มี type ในตัวแล้ว ไม่ต้องลง @types เพิ่ม
Part 7: ตัวอย่างจริงที่มีประโยชน์ — type ให้ Environment Variables
นี่คือการประยุกต์ที่ใช้บ่อยมาก — ทำให้ process.env (ตัวแปรสภาพแวดล้อมใน Node.js) มี type:
โดยปกติ process.env.ANYTHING มี type เป็น string | undefined — ไม่บอกว่ามีตัวแปรอะไรบ้าง เราเขียน declaration file เพื่อระบุ:
⚠️ ต้องมี
@types/nodeก่อน: เทคนิคนี้ทำงานได้เพราะ@types/node(npm install -D @types/node) มี interfaceNodeJS.ProcessEnvประกาศไว้อยู่แล้ว โค้ดข้างล่างแค่ "เพิ่ม" property เข้าไปด้วย declaration merging — ถ้ายังไม่ได้ติดตั้ง@types/nodeการเขียนnamespace NodeJS { ... }นี้จะกลายเป็นการสร้าง namespace ใหม่ลอย ๆ ไม่ได้ merge กับของจริง และprocess.envในโค้ดจริงจะยังไม่มี type ตามที่ต้องการ
💡 module vs script: TypeScript จะมองไฟล์เป็น "script" (ตัวแปรทุกอย่างเป็น global) ถ้าไม่มี
importหรือexportเลยสักบรรทัด — เราใช้export {}เพื่อบอก TypeScript ว่า "ไฟล์นี้เป็น module" แม้ว่าจะไม่ได้ export อะไรจริง ๆ ซึ่งจำเป็นสำหรับdeclare global { ... }ให้ทำงานถูกต้อง (กฎเดียวกันนี้จะใช้ซ้ำในตัวอย่างข้างล่าง)
💡
namespaceคืออะไร (แบบสั้นสุด):namespaceคือวิธีเก่าในการ "ห่อกลุ่มโค้ด" ให้อยู่ในชื่อเดียวกัน เช่นnamespace Foo { export const x = 1; }แล้วเรียกใช้ด้วยFoo.x— คุณไม่จำเป็นต้องเข้าใจกลไกภายในของมัน แค่จำรูปแบบข้างล่างนี้ไว้ใช้ตอน augment@types/nodeก็พอ
typescript
// ไฟล์: env.d.ts
// ⚠️ ถ้าไฟล์นี้มี import/export (กลายเป็น "module file") ต้องห่อด้วย `declare global { ... }`
// เพื่อให้ declaration merging กับ NodeJS.ProcessEnv ของ @types/node ทำงาน (ดูกฎ module vs script ด้านบน)
export {}; // ★ บรรทัดนี้ทำให้ไฟล์เป็น module — ปลอดภัยที่สุด ใช้ declare global เสมอ
declare global {
namespace NodeJS { // namespace (พื้นที่ชื่อ — ไวยากรณ์เก่า แต่จำเป็นสำหรับการ augment @types/node)
interface ProcessEnv {
DATABASE_URL: string;
PORT: string;
NODE_ENV: "development" | "production" | "test";
API_KEY?: string;
}
}
}📌 กฎเหล็ก: ถ้าไฟล์
.d.tsมีimport/exportใด ๆ → TypeScript ถือว่าเป็น "module file" → การประกาศข้างในจะ "ลอย" อยู่ในขอบเขตของไฟล์เท่านั้น ไม่ merge กับ global → ต้องห่อด้วยdeclare global { ... }ถึงจะ merge เข้ากับNodeJS.ProcessEnvของจริง💡 เกร็ด
namespace:namespaceเป็นไวยากรณ์เก่าของ TypeScript (สมัยก่อนมี ES modules) — สำหรับจัดระเบียบโค้ดใหม่ คุณแทบไม่มีโอกาสเขียนnamespaceเอง (ใช้ ES module แทน) แต่สำหรับ augment@types/nodeเช่นnamespace NodeJS { ... }นี้ —namespaceคือวิธีเดียวที่ถูกต้องและจำเป็น ไม่ใช่แค่ legacy จำรูปแบบนี้ไว้ก็พอ ไม่ต้องเข้าใจลึก
หลังจากนี้:
typescript
process.env.DATABASE_URL; // ✅ type: string — autocomplete ทำงาน
process.env.NODE_ENV; // ✅ type: "development" | "production" | "test"
process.env.TYPO_NAME; // ❌ Error! ไม่มีตัวแปรนี้ — จับการพิมพ์ผิดได้Part 8: tsconfig.json — lib และ moduleResolution (ทบทวน)
ในบทที่ 01 เราตั้งค่าพวกนี้ไปแล้ว มาทบทวนในบริบทของ module
8.1 lib — บอกว่ามี API อะไรให้ใช้
json
{
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}ES2022— ฟีเจอร์ JavaScript มาตรฐานDOM— API ของเบราว์เซอร์ (document,window)- ถ้าเขียนโปรแกรมฝั่ง Node.js ล้วน ไม่ต้องใส่
DOM— ใช้แค่["ES2022"]
8.2 moduleResolution — บอกวิธีค้นหาไฟล์ที่ import
Bundler— สำหรับโปรเจกต์ที่ใช้ bundler สมัยใหม่ (Vite) — ยืดหยุ่น import ไม่ต้องใส่นามสกุลNodeNext— สำหรับ Node.js แบบ ES Module — เข้มงวด ต้องใส่.js
⚠️ อย่าเลือกผิดตาม runtime จริง: สองตัวนี้ไม่ใช่แค่ "สไตล์เข้มงวด vs หลวม" — มันผูกกับที่ที่โค้ดจะไปรันจริง ถ้าโค้ดจะถูก Node.js รันตรง ๆ (ไม่ผ่าน bundler) แต่ตั้ง
moduleResolution: "Bundler"ไว้ —tscจะ compile ผ่านไม่ error เลย แต่พอรันจริงด้วย Node จะหา module ไม่เจอ (เพราะ Node ESM loader ต้องการนามสกุลไฟล์เสมอ) ใช้Bundlerเฉพาะเมื่อมี Vite/webpack/esbuild เป็นตัวสร้างไฟล์ผลลัพธ์จริง
8.3 ⚠️ กับดักของ Node.js ESM — ต้อง import .js
📌 มือใหม่: ถ้าคุณใช้ Vite / Next.js / tsx (เครื่องมือยุคใหม่) คุณจะไม่เจอกับดักนี้ — ข้ามไปได้ ส่วนนี้สำคัญเฉพาะเมื่อ deploy Node.js แบบ pure ESM (
moduleResolution: "NodeNext")
ถ้าใช้ moduleResolution: "NodeNext" มีกับดักที่ทำให้มือใหม่งงมาก — เวลา import ไฟล์ในโปรเจกต์ ต้องเขียนนามสกุล .js แม้ว่าไฟล์จริงจะเป็น .ts:
typescript
// ไฟล์จริงชื่อ helpers.ts — แต่ต้อง import ด้วย .js
import { formatDate } from "./helpers.js";ทำไม? เพราะ TypeScript compile helpers.ts ออกมาเป็น helpers.js — import statement จะถูกคงไว้ในไฟล์ผลลัพธ์ และตอน runtime จริง ไฟล์ที่มีอยู่คือ .js — เราจึงต้องอ้างถึงชื่อไฟล์ผลลัพธ์
(ถ้าใช้ moduleResolution: "Bundler" ไม่มีปัญหานี้ — ไม่ต้องใส่นามสกุลเลย)
💡 ความสัมพันธ์กับ
verbatimModuleSyntax:verbatimModuleSyntax(จากบทที่ 01) บังคับเรื่อง type-only import ให้ชัดเจน — แต่ ไม่บังคับ เรื่องนามสกุล.jsนั่นเป็นกฎของmoduleResolution: "NodeNext"คนละเรื่องกัน อย่าสับสน
Part 9: Lab — ลงมือทำ
Lab 1 — แบ่งโค้ดเป็น module
สร้าง 3 ไฟล์:
typescript
// ไฟล์: types.ts
export type Task = {
id: number;
title: string;
done: boolean;
};typescript
// ไฟล์: taskService.ts
import type { Task } from "./types";
export function createTask(title: string): Task {
return { id: Date.now(), title, done: false };
}
export function completeTask(task: Task): Task {
return { ...task, done: true };
}typescript
// ไฟล์: main.ts
import type { Task } from "./types";
import { createTask, completeTask } from "./taskService";
let task: Task = createTask("เรียน TypeScript");
task = completeTask(task);
console.log(task);สังเกตการใช้ import type สำหรับ Task (เป็น type) และ import ธรรมดาสำหรับฟังก์ชัน
Lab 2 — Declaration file สำหรับ asset
ฝึกเขียน declaration file (.d.ts) บอก TypeScript ว่าไฟล์ที่ไม่ใช่โค้ด (เช่น .png, .json) มี type อะไรเมื่อ import — ทำให้ import asset ได้โดยไม่ error:
typescript
// ไฟล์: assets.d.ts
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.json" {
const value: unknown;
export default value;
}Lab 3 — type ให้ env variable
typescript
// ไฟล์: env.d.ts
export {}; // ทำให้ไฟล์เป็น module → ใช้ declare global ปลอดภัย
declare global {
namespace NodeJS {
interface ProcessEnv {
APP_NAME: string;
PORT: string;
NODE_ENV: "development" | "production";
}
}
}
// ในไฟล์อื่น:
const port = Number(process.env.PORT); // ✅ มี autocomplete
const env = process.env.NODE_ENV; // type: "development" | "production"Part 10: Checkpoint
1. import type ทำอะไร ทำไมควรใช้?
บอก TypeScript ชัดเจนว่า import นี้เป็น type — จะถูกลบทิ้งตอน compile แน่นอน ช่วยให้ build ถูกต้อง (โดยเฉพาะเมื่อเปิด verbatimModuleSyntax) และโค้ดอ่านง่ายขึ้น
2. Declaration file (.d.ts) คืออะไร?
ไฟล์ที่มีแต่ "คำบรรยาย type" ไม่มีโค้ดทำงานจริง — ใช้บอก type ของโค้ด JavaScript ที่ไม่มีข้อมูล type ทำให้ TypeScript รู้จักและตรวจสอบได้
3. คำว่า declare หมายความว่าอะไร?
บอกว่า "สิ่งนี้มีอยู่จริงที่ไหนสักแห่ง (runtime) ฉันแค่มาบอก type" — ไม่มีโค้ดทำงานจากตรงนั้น
4. Ambient declaration ใช้ทำอะไร?
บอก type ให้ "ของที่มีอยู่ใน runtime แต่ TypeScript ไม่รู้จัก" เช่น global variable ที่ถูกฉีดเข้ามา หรือไฟล์ที่ไม่ใช่ .ts (svg, css)
5. Module augmentation ต่างจาก declaration file ทั่วไปยังไง?
module augmentation คือการ "เพิ่ม/ขยาย" type ของ library ที่มี type อยู่แล้ว (ด้วย declare module "ชื่อ-library") — ไม่ใช่การสร้าง type ใหม่ทั้งหมด
6. @types/* package คืออะไร?
declaration file ที่ชุมชนช่วยกันเขียนให้ library JavaScript ที่ไม่มี type มาในตัว — ติดตั้งคู่กับ library จริง เช่น npm install -D @types/lodash
7. ทำไม Node.js ESM ต้อง import ด้วยนามสกุล .js ทั้งที่ไฟล์เป็น .ts?
เพราะ TypeScript compile .ts ออกมาเป็น .js และคง import statement ไว้ — ตอน runtime ไฟล์ที่มีอยู่จริงคือ .js เราจึงต้องอ้างถึงชื่อไฟล์ผลลัพธ์
Part 11: สรุปบทนี้
- Module = ไฟล์หนึ่งไฟล์ — เชื่อมกันด้วย
export/import import type— import เฉพาะ type ถูกลบทิ้งตอน compile (ควรใช้เสมอกับ type)- Declaration file (
.d.ts) — ไฟล์ที่มีแต่ type ไม่มี implementation declare— บอกว่า "สิ่งนี้มีอยู่จริง ฉันแค่มาบอก type"- Ambient declaration — บอก type ให้ของที่ TypeScript ไม่รู้จัก (global, ไฟล์ asset)
declare global— เพิ่ม property ให้ของ global- Module augmentation — ขยาย type ของ library ที่มีอยู่
@types/*— declaration file จากชุมชนสำหรับ library JavaScript
บทถัดไป — Advanced Patterns (type-level programming)