Skip to content

บทที่ 09 — Module และ Declaration File

← บทที่ 08 | สารบัญ | บทที่ 10: Advanced Patterns →

⏱ ใช้เวลา 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 มาจากไหนได้บ้าง

  1. library สมัยใหม่ — มีมาให้ในตัว — library ที่เขียนด้วย TypeScript (เช่น React, Zod) จะ ship ไฟล์ .d.ts มาด้วย คุณไม่ต้องทำอะไร
  2. @types/* — ชุมชนช่วยเขียน — สำหรับ library JavaScript ยอดนิยม (จะพูดถึงใน Part 6)
  3. เขียนเอง — สำหรับ 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) มี interface NodeJS.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.jsonlib และ moduleResolution (ทบทวน)

ในบทที่ 01 เราตั้งค่าพวกนี้ไปแล้ว มาทบทวนในบริบทของ module

8.1 lib — บอกว่ามี API อะไรให้ใช้

json
{
  "compilerOptions": {
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  }
}
  • ES2022 — ฟีเจอร์ JavaScript มาตรฐาน
  • DOMAPI ของเบราว์เซอร์ (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)


← บทที่ 08 | สารบัญ | บทที่ 10: Advanced Patterns →