Skip to content

บทที่ 11 — TypeScript ในงานจริง

← บทที่ 10 | สารบัญ

⏱ ใช้เวลา 3–4 ชั่วโมง


บทสุดท้าย — เอาความรู้ทั้งหมดไปใช้จริง

10 บทที่ผ่านมา เราเรียน "ตัวภาษา" TypeScript จนครบ บทสุดท้ายนี้คือการ "เชื่อม" ความรู้ทั้งหมดเข้ากับงานจริง — เพราะในชีวิตการทำงาน คุณไม่ได้เขียน TypeScript ลอย ๆ แต่เขียนมันคู่กับ React, Node.js, การตรวจสอบข้อมูล, การทดสอบ ฯลฯ

บทนี้จะครอบคลุม:

  • TypeScript กับ React
  • TypeScript กับ Node.js (backend)
  • การแชร์ type ระหว่าง frontend และ backend
  • Zod — ตรวจสอบข้อมูล runtime (เราเกริ่นมาตลอดเล่ม ถึงเวลาใช้จริง)
  • การย้ายโปรเจกต์จาก JavaScript มา TypeScript
  • เครื่องมือ — linter, การทดสอบ
  • รวมข้อผิดพลาดที่พบบ่อย และ checklist สรุป

📌 เวอร์ชันที่ใช้อ้างอิงในบทนี้ (สถานะ 2026)

  • TypeScript 5.7+ — ใช้ฟีเจอร์ใหม่ เช่น --strict, noUncheckedIndexedAccess, verbatimModuleSyntax
  • Node.js 22–24 LTS — รัน TypeScript ตรงได้โดยไม่ต้อง compile ก่อน: Node 22 LTS ต้องใส่ flag --experimental-strip-types; Node 24 LTS เปิด strip-types ไว้ให้โดยอัตโนมัติ (ไม่ต้องใส่ flag) — หรือใช้ tsx/ts-node แทนก็ได้
  • React 19 — มี Server Components, use() hook, form actions (ตัวอย่างในบทนี้เน้นพื้นฐานที่ยังใช้ได้ทุกเวอร์ชัน)
  • ESM module — โปรเจกต์ใหม่นิยมตั้ง "type": "module" ใน package.json — ต้องระวังเรื่อง .js extension ใน import (อธิบายใน Part 6)
  • Zod v4, tRPC v11 — เวอร์ชันที่ใช้ในตัวอย่าง

📌 บทนี้มีหลายส่วนที่ต้องการความรู้เสริม: Part 1 ต้องการพื้นฐาน React (component, JSX, hooks), Part 2 ต้องการพื้นฐาน Node.js/Express — ถ้ายังไม่มีพื้นฐานส่วนนั้น ข้ามไปส่วนที่รู้แล้วได้ก่อน แล้วค่อยกลับมาเมื่อเรียนส่วนนั้นครบ

Part 1: TypeScript + React

💡 ผู้ที่ไม่ใช้ React ข้ามไป Part 2 ได้เลย — ส่วนนี้สมมติว่าคุณรู้พื้นฐาน React (component, JSX, hooks) มาบ้าง ถ้ายังไม่เคยใช้ ไม่ต้องตำหนิตัวเอง — กลับมาอ่านเมื่อเริ่มเรียน React ตัวอย่างทุกชิ้นต้อง import { useState, useRef } from "react" (และ React 19 ก็มี hooks ใหม่ เช่น use() ที่บทนี้ไม่ลงลึก)

React สมัยใหม่ใช้ TypeScript เป็นมาตรฐาน มาดูจุดที่ต้องใส่ type

1.1 ใส่ type ให้ Props ของ Component

Component คือฟังก์ชันที่รับ "props" (ข้อมูลขาเข้า) — เราใส่ type ให้ props เหมือนใส่ type ให้พารามิเตอร์ฟังก์ชันปกติ:

tsx
type GreetingProps = {
    name: string;
    age?: number;                  // optional
    onGreet: () => void;           // props ที่เป็นฟังก์ชัน
};

function Greeting({ name, age, onGreet }: GreetingProps) {
    return (
        <button onClick={onGreet}>
            สวัสดี {name} {age && `(อายุ ${age})`}
        </button>
    );
}

สังเกต — GreetingProps เป็นแค่ object type ธรรมดา (จากบทที่ 04) ไม่มีอะไรพิเศษ React แค่เอามาใช้กับ component

1.2 ใส่ type ให้ Hooks

Hooks ของ React หลายตัวเป็น generic — เราเติม type เข้าไปได้:

tsx
import { useState, useRef } from "react";   // อย่าลืม import

// useState — TypeScript เดา type จากค่าเริ่มต้นได้
const [count, setCount] = useState(0);            // count เป็น number (เดาเอง)

// แต่ถ้าค่าเริ่มต้นเป็น null ต้องระบุ type เอง
const [user, setUser] = useState<User | null>(null);
//                               ↑ ระบุว่า state นี้เป็น User หรือ null

// useRef — HTMLInputElement เป็น type ของ DOM element ที่ React มีให้
// (ผู้ที่ยังไม่คุ้น React ข้ามได้)
const inputRef = useRef<HTMLInputElement>(null);

จุดที่ต้องระวัง — ถ้าค่าเริ่มต้นไม่บอก type ที่แท้จริง (เช่น null, []) ต้องระบุ generic เอง ไม่งั้น TypeScript จะเดาแคบเกินไป

1.3 ใส่ type ให้ Event

เวลาจัดการ event (คลิก, พิมพ์, submit) React มี type ของ event ให้:

tsx
function Form() {
    // React.FormEvent / React.ChangeEvent คือ type ของ event ที่ React มีให้
    // (ดูเร็ว ๆ ก็พอ — บทลึก ๆ ของ React จะอธิบายแต่ละตัว)
    const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
    };

    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        console.log(event.target.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input onChange={handleChange} />
        </form>
    );
}

💡 เคล็ดลับ: ส่วนใหญ่คุณไม่ต้องจำชื่อ type ของ event — ถ้าเขียน handler ไว้ใน JSX โดยตรง (เช่น onClick={(e) => ...}) React จะ "เดา type ของ e ให้เอง" — เรียกพฤติกรรมนี้ว่า contextual typing (เดา type จากบริบทรอบข้าง) คุณค่อยใส่ type เองเมื่อต้องแยก handler ออกมาเป็นฟังก์ชันต่างหาก (เพราะตอนนั้นไม่มีบริบทแล้ว — TypeScript เดาให้ไม่ได้)

📖 เรื่อง TypeScript + React แบบลึก (เช่น React.FormEvent, React.ChangeEvent ในตัวอย่างข้างบน) มีบทเฉพาะที่ react/13 — TypeScript Deep for React (ถ้าเล่ม React ยังไม่พร้อม ข้ามไปก่อนได้)


Part 2: TypeScript + Node.js (Backend)

ฝั่ง server เราใช้ TypeScript กับ framework อย่าง Express มาดูตัวอย่าง

2.1 ใส่ type ให้ Express route

typescript
import express, { Request, Response } from "express";

const app = express();
app.use(express.json());

// กำหนด type ของข้อมูลที่ client ส่งมา
type CreateUserBody = {
    name: string;
    email: string;
};

app.post("/users", (req: Request<{}, {}, CreateUserBody>, res: Response) => {
    //                         ↑    ↑    ↑
    //                    Params ResBody ReqBody (body ที่ client ส่งมา)
    const { name, email } = req.body;   // req.body มี type เป็น CreateUserBody
    res.json({ message: `สร้างผู้ใช้ ${name} แล้ว` });
});

(generic ของ Request<Params, ResBody, ReqBody> มี 3 ช่อง — ช่องที่ 1 คือ URL params, ช่องที่ 2 คือ response body, ช่องที่ 3 คือ type ของ req.body)

💡 {} ตรงนี้คืออะไร: สังเกตว่าช่องที่ 1 และ 2 ใส่ {} ไว้ — ในบริบทนี้ {} แค่หมายถึง "เราไม่ได้ระบุ type ให้ช่องนี้ ปล่อยผ่านไปก่อน" (ไม่ได้แปลว่า "ต้องเป็น object ว่าง") เป็นการบอก Express ว่ายังไม่สนใจ type ของ URL params และ response body ในตัวอย่างนี้ — ยังต่างจาก {} ที่เป็น type ทั่วไป (ซึ่งหมายถึง "ค่าอะไรก็ได้ที่ไม่ใช่ null/undefined" ตามที่เรียนในบทที่ 02)

2.2 type ให้ Environment Variables

เราเรียนวิธีนี้ไปแล้วในบทที่ 09 — ทำให้ process.env มี type:

typescript
// ไฟล์: env.d.ts
export {};   // ★ ทำให้ไฟล์เป็น module — ต้องมีบรรทัดนี้ก่อน declare global (กฎเดียวกับบทที่ 09)

declare global {
    namespace NodeJS {
        interface ProcessEnv {
            DATABASE_URL: string;
            PORT: string;
            NODE_ENV: "development" | "production";
        }
    }
}

📌 อย่าลืม export {} + declare global ตามกฎเหล็กจากบทที่ 09 — ถ้าไฟล์นี้ดันมี import/export อื่นในภายหลัง (กลายเป็น module file) แต่ลืมห่อด้วย declare global การประกาศจะไม่ merge เข้ากับ NodeJS.ProcessEnv จริง ใช้ pattern นี้เสมอเพื่อความปลอดภัย


Part 3: แชร์ Type ระหว่าง Frontend และ Backend

นี่คือหนึ่งในประโยชน์ที่ยิ่งใหญ่ที่สุดของ TypeScript — เมื่อทั้ง frontend และ backend เขียนด้วย TypeScript เราสามารถ ใช้ type ชุดเดียวกัน ทั้งสองฝั่ง

3.1 ปัญหาที่มันแก้

ในระบบทั่วไป frontend กับ backend คุยกันผ่าน API ถ้าวันหนึ่ง backend เปลี่ยนรูปแบบข้อมูล (เช่น เปลี่ยน field name เป็น fullName) — frontend จะไม่รู้ตัว จนกระทั่งผู้ใช้เจอบั๊ก

ถ้าทั้งสองฝั่งใช้ type ชุดเดียวกัน — พอ backend เปลี่ยน type frontend จะ error ทันทีตอน compile ก่อนถึงมือผู้ใช้

3.2 วิธีที่ 1 — แชร์ type ผ่าน shared package (Monorepo)

💡 หัวข้อนี้เน้นโปรเจกต์ขนาดใหญ่ — โปรเจกต์เล็กที่ frontend/backend อยู่คนละ repo อาจไม่จำเป็นต้องตั้ง monorepo อ่านผ่าน ๆ ดูคอนเซปต์ก่อนได้

จัดโครงสร้างโปรเจกต์แบบ monorepo (โมโนรีโป—ที่เก็บโค้ดที่รวมหลาย package ไว้ในที่เดียวกัน) — มี package (แพ็กเกจ—หน่วยโค้ดที่แยกออกมาเป็นชิ้น ใช้ร่วมกันได้) กลางที่เก็บ type:

text
my-project/
├── packages/
│   ├── shared/        ← type กลาง อยู่ที่นี่
│   │   └── types.ts
│   ├── backend/       ← ใช้ type จาก shared
│   └── frontend/      ← ใช้ type จาก shared
typescript
// packages/shared/types.ts
export type User = {
    id: number;
    name: string;
    email: string;
};

ทั้ง backend และ frontend import type { User } from "shared" — ใช้ type เดียวกันเป๊ะ

3.3 วิธีที่ 2 — tRPC (type-safe แบบไม่ต้อง generate)

tRPC (อ่านว่า "ที-อาร์-พี-ซี") ย่อมาจาก Type-safe Remote Procedure Call — เทคโนโลยีที่ให้ฝั่ง client เรียกฟังก์ชันของ server ได้โดยตรง พร้อม type safety แบบ end-to-end ผลคือ frontend จะรู้จัก API ของ backend โดยอัตโนมัติ ไม่ต้องเขียน type ซ้ำ ไม่ต้อง generate อะไรเลย เวอร์ชันที่อ้างอิงในบทนี้คือ tRPC v11:

💡 อ่านผ่าน ๆ ดูคอนเซปต์ก่อนได้z.object, publicProcedure, router เป็น API เฉพาะของ tRPC + Zod ตอนใช้จริงค่อยเปิด docs ที่ trpc.io และติดตั้งด้วย npm install @trpc/server @trpc/client

typescript
// ฝั่ง server — นิยาม API
// (import ของ router / publicProcedure / z ถูกละไว้เพื่อความกระชับ — ดูวิธี setup เต็มที่ trpc.io)
const appRouter = router({
    getUser: publicProcedure
        .input(z.object({ id: z.string() }))   // validate input ด้วย Zod
        .query(({ input }) => {
            return { id: input.id, name: "Alice" };
        }),
});

export type AppRouter = typeof appRouter;   // export แค่ "type" ของ router
typescript
// ฝั่ง client — import แค่ type
import type { AppRouter } from "../server";

const trpc = createTRPCClient<AppRouter>();

const user = await trpc.getUser.query({ id: "1" });
// user มี type ครบ — TypeScript รู้เองว่ามี id, name
// ถ้าใส่ input ผิด หรือ server เปลี่ยน → error ทันที

ความเจ๋ง: client import type แค่ "type" ของ router (ซึ่งหายตอน compile) — ไม่ได้ดึงโค้ด server มาเลย แต่ได้ type safety เต็มที่

💡 ทางเลือกอื่นที่น่ารู้ (สถานะ 2026) — ถ้าสนใจทางเลือกอื่นนอกจาก tRPC มีตัวที่นิยมในปี 2026 ดังนี้ — อ่านผ่าน ๆ ก็พอ ไม่ต้องจำทั้งหมด:

  • Hono RPC — Hono เป็น web framework เบา ๆ ที่รันได้บนหลาย runtime (สภาพแวดล้อมที่ใช้รันโค้ด JavaScript): Node.js, Bun (runtime ทางเลือกจากผู้สร้างรายใหม่), Deno (อีก runtime ทางเลือก), และ Edge runtime (รัน JS ใกล้ผู้ใช้บน CDN) — ใช้ hc<typeof app>() ฝั่ง client ได้ type safety แบบ tRPC
  • ts-rest / oRPC — ทางเลือก type-safe อื่น ๆ ที่ออกแบบให้คล้าย REST มากกว่า
  • OpenAPI + generator — ถ้าทีมมี backend ภาษาอื่น (Java, Go, Python) ใช้ OpenAPI spec (ไฟล์อธิบาย API มาตรฐาน) แล้ว generate type ฝั่ง TS ได้ (เช่น openapi-typescript)

Part 4: Zod — ตรวจสอบข้อมูล Runtime

เรากล่าวถึง Zod มาตลอดทั้งเล่ม ถึงเวลาใช้จริงเต็ม ๆ

4.1 ทบทวนว่าทำไมต้องมี Zod

จำบทที่ 00 ได้ไหม — type ของ TypeScript หายไปตอน runtime (type erasure) ดังนั้นข้อมูลที่มาจากภายนอก (API, ฟอร์ม, ไฟล์) TypeScript ตรวจสอบให้ไม่ได้:

typescript
const user: User = await fetch("/api/user").then(r => r.json());
// TypeScript "เชื่อ" ว่าเป็น User — แต่ถ้า server ส่งข้อมูลผิดมา โปรแกรมจะพังตอน runtime

Zod แก้ปัญหานี้ — มันตรวจสอบข้อมูล จริง ๆ ตอน runtime

4.2 พื้นฐาน Zod

Zod เป็น library ตรวจสอบข้อมูลตอน runtime (เช่น response จาก API ที่ TypeScript เชื่อไม่ได้ว่าตรง type จริง) — ประกาศ "schema" ของข้อมูลแล้ว Zod ทั้ง validate และ infer type ให้ ปิดช่องว่างระหว่าง compile-time กับ runtime เวอร์ชันที่อ้างอิงในบทนี้คือ Zod v4 (API ส่วนใหญ่ตรงกับ v3 — แต่ v4 ปรับ error format และเร็วขึ้น):

typescript
import { z } from "zod";

// 1. ประกาศ "schema" — กฎว่าข้อมูลต้องหน้าตาแบบไหน
const UserSchema = z.object({
    id: z.number(),
    name: z.string().min(2),                    // string ยาวอย่างน้อย 2 ตัว
    email: z.string().email(),                  // ต้องเป็นรูปแบบอีเมล
    age: z.number().min(0).max(150).optional(), // ตัวเลข 0-150 ไม่บังคับ
    role: z.enum(["admin", "user", "guest"]),   // ต้องเป็นค่าใดค่าหนึ่งใน 3 ตัว
});

// 2. ดึง type ของ TypeScript ออกจาก schema — ไม่ต้องเขียน type ซ้ำ!
type User = z.infer<typeof UserSchema>;

4.3 ตรวจสอบข้อมูลจริง

Zod มี 2 วิธีตรวจ:

typescript
// วิธีที่ 1 — parse() — ถ้าข้อมูลผิด จะ "โยน error"
const user = UserSchema.parse(rawData);   // ถ้าผ่าน user มี type เป็น User

// วิธีที่ 2 — safeParse() — คืน result ไม่โยน error (จัดการเองได้)
const result = UserSchema.safeParse(rawData);
if (result.success) {
    console.log(result.data);    // ข้อมูลที่ผ่านการตรวจ มี type เป็น User
} else {
    console.log(result.error);   // รายละเอียดว่าผิดตรงไหน
}

result ของ safeParse เป็น discriminated union (จากบทที่ 04/07) — result.success คือ discriminant: ถ้า true TypeScript รู้ว่า result.data มีค่า ถ้า false TypeScript รู้ว่า result.error มีค่า — ดูบทที่ 07 ถ้าต้องการทบทวน

4.4 ใช้จริง — ตรวจข้อมูลที่ API รับมา

typescript
app.post("/users", (req, res) => {
    const result = UserSchema.safeParse(req.body);

    if (!result.success) {
        // ข้อมูลที่ client ส่งมาไม่ถูกต้อง — ตอบ error
        return res.status(400).json({ error: "ข้อมูลไม่ถูกต้อง" });
    }

    const user = result.data;   // ตรงนี้ปลอดภัยแล้ว — มี type เป็น User
    // ...บันทึก user ลงฐานข้อมูล
});

🎯 กฎทอง: ทุกจุดที่ข้อมูล "เข้าสู่โปรแกรม" จากภายนอก — API request, response, ฟอร์ม, localStorage, ตัวแปร env — ให้ ตรวจด้วย Zod อย่าเชื่อ as หรือเชื่อ type ลอย ๆ นี่คือเส้นแบ่งระหว่างโปรแกรมที่ "type-safe จริง" กับ "type-safe แค่บนกระดาษ"

💡 ทางเลือกอื่น (สถานะ 2026) — Zod ไม่ใช่ตัวเลือกเดียว:

  • ValibotAPI คล้าย Zod แต่ออกแบบให้ tree-shake (ตัดโค้ดที่ไม่ใช้ออก) ได้ดีกว่า ทำให้ bundle (ไฟล์รวมที่ส่งให้เบราว์เซอร์) เล็กมาก เหมาะกับ frontend ที่แคร์ขนาดไฟล์
  • arktype — ใช้ syntax คล้าย TypeScript type เลย (type({ name: "string" })) เร็วและ infer type ได้สวย
  • Effect Schema — เป็นส่วนหนึ่งของ Effect ecosystem (กลุ่ม library สไตล์ functional programming) เหมาะถ้าทีมใช้ Effect อยู่แล้ว
  • TypeBox — ใช้ JSON Schema เป็นพื้น เหมาะถ้าต้องการสร้าง OpenAPI spec (ไฟล์อธิบาย API มาตรฐาน)

เลือกตัวที่เข้ากับทีมและโปรเจกต์ — แนวคิด "validate ที่ขอบ" เหมือนกันทุกตัว


Part 5: ย้ายโปรเจกต์จาก JavaScript มา TypeScript (Migration — การย้าย/อพยพ)

ถ้าคุณมีโปรเจกต์ JavaScript เดิมอยู่ คุณไม่ต้องเขียนใหม่ทั้งหมด — ย้ายได้ทีละนิด

5.1 กลยุทธ์ — ค่อยเป็นค่อยไป

อย่าพยายามแปลงทั้งโปรเจกต์ในครั้งเดียว ทำตามลำดับนี้:

  1. ติดตั้ง TypeScript และสร้าง tsconfig.json โดยเปิด allowJs: true (ให้ไฟล์ .js กับ .ts อยู่ด้วยกันได้) และเริ่มด้วย strict: false ก่อน
  2. เปลี่ยนชื่อไฟล์ทีละไฟล์ จาก .js เป็น .ts — เริ่มจากไฟล์เล็ก ๆ ที่ไม่มีใครพึ่งพา (utility, helper)
  3. แก้ error ที่เกิดขึ้น ในไฟล์นั้นจนหมด
  4. ค่อย ๆ เพิ่มความเข้มงวด — เมื่อแปลงไฟล์ได้พอสมควรแล้ว ค่อยเปิด noImplicitAny → แก้ → เปิด strictNullChecks → แก้ → สุดท้ายเปิด strict: true
  5. เปิด setting แนะนำเพิ่ม (เมื่อโค้ดส่วนใหญ่เป็น TS แล้ว) — ดูหัวข้อ 5.4 ด้านล่าง

5.4 tsconfig settings ที่แนะนำ (สถานะ 2026)

เมื่อย้ายเสร็จแล้ว แนะนำให้เปิด setting เหล่านี้ใน tsconfig.json:

json
{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "verbatimModuleSyntax": true,
        "target": "ES2022",
        "module": "NodeNext",
        "moduleResolution": "NodeNext"
    }
}

อธิบายแต่ละตัว:

  • strict: true — เปิด strict mode ทั้งชุด (รวม noImplicitAny, strictNullChecks, strictFunctionTypes ฯลฯ) ไม่ต่อรอง — ค่านี้คือเส้นแบ่งระหว่าง "เขียน TypeScript จริง" กับ "เขียน JavaScript แต่งกลิ่น"
  • noUncheckedIndexedAccess: true — บังคับให้ access array/object ด้วย index คืน type T | undefined (เพราะ index อาจอยู่นอกขอบ array หรือ key อาจไม่มีจริง) ทำให้ต้องเช็ก undefined ก่อนใช้ ปลอดภัยขึ้นมาก:
    typescript
    const arr = [1, 2, 3];
    const first = arr[0];    // ปกติ: number — เปิดแล้ว: number | undefined
    // first.toFixed(2);     // ❌ error — ต้องเช็ก undefined ก่อน
    if (first !== undefined) first.toFixed(2);   // ✅
  • verbatimModuleSyntax: true — บังคับให้ import type ที่จะใช้แค่ระดับ type ต้องเขียน import type ชัด ๆ (ไม่เขียน TS จะลบให้เอง) ช่วยให้ผลของ tsc ตรงกับสิ่งที่เขียน — สำคัญมากเมื่อใช้ ESM
  • target: "ES2022" + module/moduleResolution: "NodeNext" — เหมาะกับ Node.js 22-24 + ESM

5.2 ทางลัด — ใช้ JSDoc ก่อน (ยังไม่ต้องเปลี่ยนนามสกุล)

ถ้ายังไม่อยากเปลี่ยนนามสกุลไฟล์ คุณ "ได้ type check" ในไฟล์ .js ได้ด้วยการเขียน comment แบบ JSDoc:

javascript
// ไฟล์: math.js (ยังเป็น .js)
/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function add(a, b) {
    return a + b;
}

แล้วเปิดใน tsconfig.json:

json
{ "allowJs": true, "checkJs": true }

TypeScript จะ "อ่าน" JSDoc แล้วตรวจ type ให้ — add("5", 3) จะ error แม้ในไฟล์ .js

5.3 พัก error ชั่วคราว — // @ts-expect-error

ระหว่างย้าย คุณอาจเจอ error ที่ "ยังไม่อยากแก้ตอนนี้" ใช้ comment นี้พักไว้ก่อน:

typescript
// @ts-expect-error — โค้ดนี้ยังไม่มี type จะแก้ทีหลัง
const result = oldUntypedFunction();

@ts-expect-error ทำให้ TypeScript "ข้าม" error บรรทัดถัดไป — และข้อดีคือ ถ้าวันหนึ่งบรรทัดนั้นไม่มี error แล้ว TypeScript จะเตือนว่า "ลบ comment นี้ได้แล้ว" ทำให้เราไม่ลืมเก็บกวาด

(มี // @ts-ignore ที่คล้ายกัน แต่ @ts-expect-error ดีกว่าเพราะมันเตือนเมื่อไม่จำเป็นแล้ว)


Part 6: เครื่องมือในงานจริง

6.1 Linter — typescript-eslint (ESLint 9 flat config)

Linter (ลินเทอร์ — เครื่องมือที่ตรวจ "คุณภาพโค้ด" นอกเหนือจาก type) เตือนตัวแปรที่ประกาศแล้วไม่ใช้ เตือน pattern ที่เสี่ยง ตั้งแต่ ESLint 9 เป็นต้นไป ค่าตั้งต้นใช้ flat config (eslint.config.js) ไม่ใช่ .eslintrc แบบเดิม

bash
npm install -D eslint typescript-eslint
javascript
// eslint.config.js (ESLint 9 flat config)
import tseslint from "typescript-eslint";

export default tseslint.config(
    ...tseslint.configs.recommendedTypeChecked,
    {
        languageOptions: {
            parserOptions: {
                projectService: true,   // อ่าน tsconfig อัตโนมัติ (TS 5.7+ แนะนำ)
            },
        },
    }
);

typescript-eslint มี rule ที่เข้าใจ type — เช่น เตือนถ้าคุณ await สิ่งที่ไม่ใช่ Promise

💡 Biome — ทางเลือกใหม่ที่กำลังมาแรงBiome เป็น linter + formatter ตัวเดียวเขียนด้วย Rust เร็วกว่า ESLint + Prettier มาก โปรเจกต์ใหม่หลายเจ้าใช้ Biome คู่กับ Prettier (Biome ทำหน้าที่ lint, Prettier ทำหน้าที่ format) — หรือใช้ Biome ตัวเดียวทั้งคู่ก็ได้ ยังครอบคลุม rule ไม่ครบเท่า ESLint แต่กำลังพัฒนาเร็ว

6.2 การทดสอบ — Vitest (แทน Jest)

Vitest เป็นเครื่องมือทดสอบยอดนิยมที่เข้ากับ TypeScript ดี — สถานะ 2026 Vitest เป็น default ของโปรเจกต์ใหม่ ส่วนใหญ่ (แทนที่ Jest ที่เคยครองตลาด) เพราะ:

  • เร็วกว่ามาก (ใช้ Vite/esbuild)
  • API ส่วนใหญ่เข้ากันได้กับ Jest — migrate ง่าย
  • รองรับ ESM และ TypeScript ตรง ๆ ไม่ต้องตั้ง Babel/ts-jest
typescript
import { describe, it, expect } from "vitest";
import { calculateTotal } from "./cart.js";   // ESM ต้องมี .js extension! (ดู 6.4)

describe("calculateTotal", () => {
    it("คำนวณราคารวมถูกต้อง", () => {
        expect(calculateTotal(100, 3)).toBe(300);
    });
});

6.3 แยกหน้าที่ของเครื่องมือ build

จำจากบทที่ 01 — "ตรวจ type" กับ "สร้างไฟล์" เป็นคนละงาน:

  • tsc --noEmit — ตรวจ type อย่างเดียว ช้าแต่แม่นยำ → ใช้ใน CI (Continuous Integration — ระบบรวมและตรวจโค้ดอัตโนมัติทุกครั้งที่มีคนส่งโค้ดใหม่) ก่อน merge (การ "รวม" สาขาโค้ดของเราเข้ากับโค้ดหลัก เช่น branch main ของทีม) เข้าโปรเจกต์หลัก
  • Vite / esbuild / Bun — สร้างไฟล์จริง เร็วมาก (ลบ type ทิ้ง ไม่ตรวจ) → ใช้ build

ใน CI ควรมีขั้นตอน tsc --noEmit เสมอ เพื่อ "บล็อก" code ที่มี type error ไม่ให้เข้าโปรเจกต์

6.4 ESM module และเรื่อง .js extension

สถานะ 2026 โปรเจกต์ TypeScript ใหม่ส่วนใหญ่ใช้ ESM (ECMAScript Module — ระบบ module มาตรฐานของ JavaScript) ไม่ใช่ CommonJS แบบเดิม ตั้งใน package.json:

json
{ "type": "module" }

กับดักสำคัญ — ใน ESM เมื่อ import ไฟล์ในโปรเจกต์ตัวเอง (relative import) ต้องใส่นามสกุล .js ต่อท้าย แม้ไฟล์จริงจะเป็น .ts:

typescript
// ❌ CommonJS style — error ใน ESM
import { calculateTotal } from "./cart";

// ✅ ESM — ต้องมี .js (แม้ไฟล์จริงคือ cart.ts!)
import { calculateTotal } from "./cart.js";

เหตุผล: TypeScript ไม่เขียน .ts.js ให้ตอน compile (compiler ออกแบบให้เป็นแบบนั้นโดยตั้งใจ) ดังนั้นต้องเขียน .js ในซอร์สเลย — verbatimModuleSyntax ที่เปิดไว้ใน Part 5.4 ก็ช่วยจับเคสนี้

6.5 Barrel file (ไฟล์รวม export กลาง)

Barrel file (อ่าน "บาร์เรล" = "ถัง" — ถังที่รวม export หลายไฟล์ไว้ในที่เดียว) คือไฟล์ index.ts ที่ re-export ของจากหลายไฟล์ในโฟลเดอร์เดียว ทำให้ import จากภายนอกสะดวก:

typescript
// src/utils/index.ts (barrel file)
export { formatDate } from "./date.js";
export { slugify } from "./string.js";
export type { User } from "./user.js";
typescript
// ที่อื่น — import ครั้งเดียวได้หลายตัว
import { formatDate, slugify, type User } from "./utils/index.js";

⚠️ ข้อเสียที่ควรรู้: barrel file ทำให้ tree-shaking (การลบโค้ดที่ไม่ได้ใช้) ทำงานยากขึ้น — bundle อาจใหญ่กว่าที่ควร และทำให้ vitest/jest โหลดไฟล์เยอะตอนเทสต์ — โปรเจกต์ใหญ่บางทีมเลิกใช้ barrel แล้วให้ import จากไฟล์ตรง ๆ แทน ชั่งน้ำหนัก "ความสะดวก" กับ "ขนาด bundle" ดู


Part 7: ข้อผิดพลาดที่พบบ่อย (และวิธีแก้)

รวมข้อผิดพลาดที่มือใหม่ทำบ่อย — เช็กตัวเองว่าหลีกเลี่ยงได้ไหม

7.1 ใช้ any พร่ำเพรื่อ

ข้อผิดพลาดอันดับหนึ่ง — ใส่ any เพื่อให้ error หาย ซึ่งปิด type checking ทั้งหมด (เสียประโยชน์ของ TypeScript) ถ้าไม่รู้ type จริงให้ใช้ unknown ที่บังคับให้ narrow ก่อนใช้ ปลอดภัยกว่า:

typescript
function process(data: any) { /* ... */ }      // ❌ เสีย type safety ทั้งหมด
function process(data: unknown) { /* ... */ }  // ✅ ปลอดภัยกว่า ต้อง narrow ก่อนใช้

7.2 ใช้ as เป็นทางลัด

as (type assertion) คือการ "บอก TypeScript ให้เชื่อ" ว่าค่าเป็น type นั้นโดยไม่ตรวจจริง — อันตรายกับข้อมูลภายนอก (API) ที่อาจไม่ตรง ควร validate จริงด้วย Zod แทน:

typescript
const user = response as User;              // ❌ แค่ "เชื่อ" ไม่ได้ตรวจจริง
const user = UserSchema.parse(response);    // ✅ ตรวจจริงด้วย Zod

7.3 ใส่ ? (optional) ทั้งที่ไม่ควร optional

typescript
type User = {
    name?: string;   // ❌ ถ้าในความเป็นจริง user ต้องมีชื่อเสมอ ก็ไม่ควรเป็น optional
};

ใส่ ? เฉพาะ field ที่ "ขาดได้จริง ๆ" ตามตรรกะธุรกิจ — การใส่ ? มั่ว ๆ ทำให้ต้องเช็ก undefined ทุกที่โดยไม่จำเป็น

7.4 ปิด strict เพราะ error เยอะ

อย่าทำ — error ที่ strict ฟ้องคือบั๊กจริงที่รอระเบิด การปิด strict ไม่ได้ทำให้บั๊กหายไป แค่ซ่อนมันไว้


Part 8: Checklist สรุป — TypeScript ที่ดี

เมื่อเริ่มโปรเจกต์ใหม่ หรือ review โค้ด ใช้ checklist นี้:

การตั้งค่า

  • ☐ เปิด strict: true + noUncheckedIndexedAccess
  • ☐ มีขั้นตอน tsc --noEmit ใน CI

การเขียน type

  • ☐ เลี่ยง any — ใช้ unknown แทน
  • ☐ เลี่ยง as — ใช้ type guard หรือ Zod
  • ☐ ใช้ ?. และ ?? จัดการ null/undefined
  • ☐ ใช้ discriminated union สำหรับ state ที่มีหลายแบบ
  • ☐ ใช้ as const satisfies สำหรับ object คงที่
  • ☐ ใส่ ? เฉพาะ field ที่ optional จริง ๆ
  • ☐ ใช้ exhaustive check (never) กับ switch ของ union

ข้อมูลภายนอก

  • ☐ ตรวจข้อมูลจาก API / ฟอร์ม / env ด้วย Zod เสมอ

โครงสร้าง

  • ☐ ใช้ import type กับ type
  • ☐ แชร์ type ระหว่าง frontend/backend (shared package หรือ tRPC)
  • ☐ เขียน comment อธิบาย type ที่ซับซ้อน

Part 9: Lab — ลงมือทำ

Lab 1 — Zod ครบวงจร

ฝึกใช้ Zod ครบวงจร — ประกาศ schema, infer type จาก schema, validate ข้อมูลขาเข้า แล้วใช้ type ที่ได้อย่างปลอดภัย เห็นภาพการเชื่อม runtime validation กับ static type เข้าด้วยกัน:

typescript
import { z } from "zod";

// schema สำหรับฟอร์มสมัครสมาชิก
const SignupSchema = z.object({
    username: z.string().min(3, "ชื่อผู้ใช้ต้องยาวอย่างน้อย 3 ตัว"),
    email: z.string().email("รูปแบบอีเมลไม่ถูกต้อง"),
    password: z.string().min(8, "รหัสผ่านต้องยาวอย่างน้อย 8 ตัว"),
    age: z.number().min(13, "ต้องอายุ 13 ปีขึ้นไป"),
});

type SignupData = z.infer<typeof SignupSchema>;

function handleSignup(rawInput: unknown): SignupData | null {
    const result = SignupSchema.safeParse(rawInput);
    if (!result.success) {
        console.log("ข้อมูลไม่ถูกต้อง:", result.error.issues[0]?.message);
        return null;
    }
    return result.data;
}

console.log(handleSignup({ username: "al", email: "x", password: "1", age: 10 }));
console.log(handleSignup({ username: "alice", email: "a@x.com", password: "12345678", age: 25 }));

Lab 2 — Type-safe form state

💡 Lab นี้ใช้ mapped type จากบทที่ 10 — ถ้าข้ามบท 10 มา ให้กลับไปอ่าน Part 1 ของบทนั้นก่อน หรือดูตัวอย่างเป็น "ของแถม" ก็ได้

typescript
// แปลง type ข้อมูล ให้กลายเป็น type ของ "สถานะฟอร์ม"
type FormState<T> = {
    [K in keyof T]: {
        value: T[K];
        error?: string;
        touched: boolean;
    };
};

type LoginInput = { email: string; password: string };
type LoginFormState = FormState<LoginInput>;
// {
//     email: { value: string; error?: string; touched: boolean };
//     password: { value: string; error?: string; touched: boolean };
// }

const initialState: LoginFormState = {
    email: { value: "", touched: false },
    password: { value: "", touched: false },
};

(สังเกตว่าเราใช้ mapped type จากบทที่ 10 มาทำงานจริง)

Lab 3 — วางแผน migration

ถ้าคุณมีโปรเจกต์ JavaScript ลองเขียนแผนย้ายเป็น TypeScript:

  1. ติดตั้ง typescript, สร้าง tsconfig.json (allowJs: true, strict: false)
  2. เปิด checkJs แล้วเขียน JSDoc ให้ไฟล์หลัก 3–5 ไฟล์
  3. เปลี่ยน 1 ไฟล์เป็น .ts แก้ error จนหมด
  4. ทำซ้ำข้อ 3 ทีละไฟล์
  5. เปิด noImplicitAny → แก้ → เปิด strict: true

Part 10: Checkpoint

1. ใส่ type ให้ React component props ยังไง?

สร้าง object type ธรรมดา (เช่น type Props = { name: string }) แล้วใช้กับพารามิเตอร์ของฟังก์ชัน component — เหมือนใส่ type ให้พารามิเตอร์ฟังก์ชันปกติ

2. ทำไมการแชร์ type ระหว่าง frontend/backend ถึงมีประโยชน์?

เมื่อทั้งสองฝั่งใช้ type ชุดเดียวกัน — ถ้า backend เปลี่ยนรูปแบบข้อมูล frontend จะ error ทันทีตอน compile ก่อนถึงมือผู้ใช้

3. ทำไมต้องใช้ Zod ทั้งที่มี TypeScript?

เพราะ type ของ TypeScript หายตอน runtime — ข้อมูลจากภายนอก (API, ฟอร์ม) TypeScript ตรวจให้ไม่ได้ Zod ตรวจสอบจริง ๆ ตอน runtime และ infer type ให้ด้วย

4. parse() กับ safeParse() ของ Zod ต่างกันยังไง?

parse() ถ้าข้อมูลผิดจะโยน error safeParse() คืน result object ({ success: true/false, ... }) ให้เราจัดการเองโดยไม่โยน error

5. กลยุทธ์ย้าย JavaScript → TypeScript ที่ดีคืออะไร?

ค่อยเป็นค่อยไป — เปิด allowJs, เริ่ม strict: false, เปลี่ยนไฟล์ทีละไฟล์จากเล็กไปใหญ่, ค่อย ๆ เพิ่มความเข้มงวด (noImplicitAnystrictNullChecksstrict)

6. // @ts-expect-error ดีกว่า // @ts-ignore ตรงไหน?

@ts-expect-error จะเตือนเราเมื่อบรรทัดนั้นไม่มี error แล้ว (บอกว่าลบ comment ได้) ทำให้ไม่ลืมเก็บกวาด ส่วน @ts-ignore ไม่เตือน

7. ทำไมไม่ควรปิด strict แม้ error เยอะ?

error ที่ strict ฟ้องคือบั๊กจริงที่รอจะระเบิดตอน runtime — การปิด strict ไม่ได้ทำให้บั๊กหาย แค่ซ่อนมันไว้


Part 11: สรุปบทนี้ และจบเล่ม

สรุปบทนี้

  • React — ใส่ type ให้ props (object type ธรรมดา), hooks (generic), event
  • Node.js — ใส่ type ให้ route, env variables
  • แชร์ type frontend/backend — shared package หรือ tRPC → จับบั๊ก API ตั้งแต่ compile
  • Zod — ตรวจสอบข้อมูลภายนอกตอน runtime — กฎทอง: ตรวจทุกจุดที่ข้อมูลเข้าโปรแกรม
  • Migration — ค่อยเป็นค่อยไป ทีละไฟล์ ค่อย ๆ เพิ่มความเข้มงวด
  • เครื่องมือ — ESLint 9 (flat config) / Biome เป็น linter, Vitest แทน Jest, tsc --noEmit ใน CI
  • tsconfig แนะนำstrict, noUncheckedIndexedAccess, verbatimModuleSyntax
  • ESM — ใส่ .js ใน relative import แม้ไฟล์จริงเป็น .ts
  • เลี่ยงany, as มั่ว, ? เกินจำเป็น, ปิด strict

🎉 จบเล่ม TypeScript

ขอแสดงความยินดี! คุณเดินทางครบทั้ง 12 บท (รวมบท 0) — จาก let x: number = 5 ในบทแรก จนถึง template literal type และ branded type ในบทท้าย ๆ

สิ่งที่คุณทำได้ตอนนี้:

  • เข้าใจว่า TypeScript ทำงานยังไง และทำไมมันถึงสำคัญ
  • เขียน type ให้โค้ดได้อย่าง type-safe และ idiomatic (อิดิโอแมติก—เป็นธรรมชาติตามสไตล์ที่ภาษานั้นนิยมเขียนกัน)
  • เข้าใจ generic, utility types, narrowing — เครื่องมือระดับกลาง
  • อ่าน type ขั้นสูง (conditional, mapped, infer) ของ library ดัง ๆ ออก
  • เอา TypeScript ไปใช้กับงานจริง — React, Node, การตรวจสอบข้อมูล

ก้าวต่อไป

การเรียนรู้ไม่จบแค่นี้ — ขั้นต่อไปที่แนะนำ:

  • ฝึกฝน type ที่ Type Challenges หรือ typehero.dev
  • อ่านเอกสารทางการTypeScript Handbook
  • ติดตาม release notes ของ TypeScript ทุกไตรมาส — ภาษามีของใหม่เสมอ
  • อ่านโค้ด .d.ts ของ library ดัง ๆ เช่น React, Zod — เรียนรู้จากของจริง
  • ลงมือทำโปรเจกต์จริง — ความเชี่ยวชาญมาจากการเขียน ไม่ใช่การอ่าน

💬 ข้อคิดส่งท้าย: TypeScript ไม่ได้มีไว้ทำให้การเขียนโค้ดยากขึ้น — มันมีไว้ทำให้คุณ มั่นใจ ในโค้ดที่เขียน ทุก type ที่คุณใส่คือคำสัญญาที่ TypeScript จะคอยช่วยตรวจสอบให้ตลอดอายุของโปรเจกต์ ขอให้สนุกกับการเขียน TypeScript ครับ 🚀


← บทที่ 10 | สารบัญ