TypeScript Best Practices: เขียนโค้ดให้ปลอดภัยและดูแลรักษาได้ง่ายขึ้น
เรียนรู้ pattern ของ TypeScript ที่สำคัญที่สุด — strict mode, utility types, generics, discriminated unions และการหลีกเลี่ยง escape hatch ในระบบ type ที่พบบ่อย
TypeScript ได้เปลี่ยนสถานะจาก "ตัวเลือกเสริม" มาเป็น "สิ่งจำเป็น" ในโปรเจกต์ JavaScript ระดับมืออาชีพส่วนใหญ่ แต่ยังมีช่องว่างขนาดใหญ่ระหว่าง "TypeScript ที่ใช้งานได้ในทางเทคนิค" กับ "TypeScript ที่จับ bug ได้จริง" ความแตกต่างนั้นอยู่ที่ pattern เพียงไม่กี่อย่างที่ปลดล็อคพลังที่แท้จริงของระบบ type
เปิดใช้ strict mode — เสมอ
การเปลี่ยนแปลง TypeScript configuration ที่คุ้มค่าที่สุดในบรรดาทั้งหมด:
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strict เปิดใช้งานการตรวจสอบหลายอย่างพร้อมกัน:
strictNullChecks—nullและundefinedไม่สามารถกำหนดให้กับ type อื่นได้noImplicitAny— ตัวแปรไม่สามารถมี type เป็นanyโดยปริยายstrictFunctionTypes— ตรวจสอบ parameter ของฟังก์ชันอย่างเข้มงวดขึ้นstrictPropertyInitialization— property ของ class ต้องถูก initialize เสมอ
หากไม่มี strictNullChecks TypeScript จะพลาด runtime error ที่พบบ่อยที่สุด เปิดใช้ตั้งแต่วันแรก — การย้อนกลับมาเพิ่มใน codebase ขนาดใหญ่นั้นเจ็บปวดมาก
หลีกเลี่ยง any — ใช้ unknown แทน
any คือ escape hatch ของระบบ type ที่ปิดเสียง error ทั้งหมดและทำให้ TypeScript สูญเสียประโยชน์ไป:
// ❌ any disables all type checking
function processData(data: any) {
data.nonExistentMethod(); // No error! Crashes at runtime.
}
// ✅ unknown forces you to narrow the type before using it
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // Safe — narrowed to string
}
if (data instanceof Error) {
console.log(data.message); // Safe — narrowed to Error
}
}
unknown คือเวอร์ชันที่ปลอดภัยของ any มันบังคับให้คุณตรวจสอบก่อนว่ามีอะไรอยู่ก่อนที่จะใช้งาน ใช้กับ API response, การ parse JSON และทุกอย่างที่มาจากแหล่งข้อมูลภายนอก
Discriminated unions สำหรับการจัดการแบบครบถ้วน
หนึ่งใน pattern ที่ทรงพลังที่สุดของ TypeScript:
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // state.data available here
case "error":
return <ErrorMessage msg={state.message} />; // state.message available here
default:
// TypeScript will error here if you add a new state without handling it
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
การกำหนดค่า never ที่ท้ายสุดคือ exhaustiveness check — ถ้าคุณเพิ่ม variant ใหม่ใน ApiState โดยไม่ได้จัดการใน switch TypeScript จะแจ้ง error ตั้งแต่ compile time นี่คือ safety net ที่มีค่าที่สุดอย่างหนึ่งในภาษานี้
Utility types
TypeScript มี type transformer ในตัวมาให้:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
// Make all properties read-only
type ReadonlyUser = Readonly<User>;
// Pick specific properties
type UserPreview = Pick<User, "id" | "name">;
// Exclude specific properties
type UserWithoutDates = Omit<User, "createdAt">;
// Create a type from an object's values
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// Make properties nullable
type NullableUser = { [K in keyof User]: User[K] | null };
// Record type: object with specific key and value types
type RolePermissions = Record<UserRole, string[]>;
// Extract/Exclude from union types
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
Generics สำหรับโค้ดที่ใช้ซ้ำได้และปลอดภัยในด้าน type
Generics ช่วยให้คุณเขียนฟังก์ชันและ class ที่ทำงานได้กับทุก type โดยยังคงความปลอดภัยด้าน type อย่างสมบูรณ์:
// Without generics — loses type information
function first(arr: any[]): any {
return arr[0];
}
// With generics — preserves type
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type: number | undefined
const str = first(["a", "b"]); // type: string | undefined
การกำหนดขอบเขตของ generics
// T must have an 'id' property
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// K must be a key of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // type: string
const role = getProperty(user, "role"); // type: string
// getProperty(user, "missing"); // ❌ TypeScript error — "missing" not a key
Type guards และการ narrowing
// Type predicate — custom type guard
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// Usage
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScript knows data is User here
}
สำหรับการตรวจสอบ API response ขณะ runtime ให้ใช้ TypeScript interfaces คู่กับ runtime schema validation ด้วย Zod สร้าง Zod schema จาก JSON ของคุณได้โดยตรงด้วย converter JSON to Zod — วาง JSON response ตัวอย่างแล้วรับ schema ที่ปลอดภัยทาง type ได้ทันที
Template literal types
TypeScript 4.1+ รองรับการจัดการ string ในระดับ type:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
operator satisfies (TypeScript 4.9+)
ตรวจสอบว่าค่าตรงกับ type โดยไม่ขยาย type ออก:
type ColorMap = Record<string, string>;
// ❌ Without satisfies — type widened to Record<string, string>
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // Works but loses string type
// ✅ With satisfies — validates AND preserves literal types
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScript knows it's a string
colors.nonexistent; // ❌ Error — key doesn't exist
การสร้าง type จากข้อมูล
แทนที่จะเขียน TypeScript interface ด้วยมือจาก API response:
- วาง JSON response ตัวอย่างใน converter JSON to TypeScript เพื่อรับ interface ทันที
- หรือสร้าง Zod schema ด้วย JSON to Zod — Zod อนุมาน TypeScript type โดยอัตโนมัติผ่าน
z.infer<typeof schema>
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// Type inferred automatically — no duplication
type User = z.infer<typeof UserSchema>;
// Validates at runtime AND provides TypeScript types
const user = UserSchema.parse(await response.json());
Anti-pattern ที่ควรหลีกเลี่ยง
| Anti-pattern | ปัญหา | วิธีแก้ไข |
|---|---|---|
as any |
ปิดการตรวจสอบ type | ใช้ unknown + narrowing |
// @ts-ignore |
ปิดเสียง error โดยไม่แก้ไข | แก้ไข type error ให้ถูกต้อง |
as SomeType (unsafe cast) |
ข้าม inference | ใช้ type guards |
interface สำหรับ union |
ไม่รองรับ discriminated unions | ใช้ type แทน |
object ที่กว้างเกินไป |
ไม่มีโครงสร้าง | กำหนด interface ที่เหมาะสม |
Function เป็น type |
ไม่มีข้อมูล parameter/return | ใช้ signature ที่ระบุชัดเจน |
TypeScript configuration ที่ควรรู้จัก
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n] returns T | undefined
"exactOptionalPropertyTypes": true, // distinguish missing vs undefined
"noImplicitReturns": true, // all code paths must return
"noFallthroughCasesInSwitch": true, // no unintentional switch fallthrough
"verbatimModuleSyntax": true // explicit import type for type-only imports
}
}
คุณค่าของ TypeScript นั้นแปรผันตามความเข้มงวดในการใช้งาน pattern ที่กล่าวถึงที่นี่ — strict mode, discriminated unions, generics, exhaustiveness checks — คือสิ่งที่แยก TypeScript ที่ช่วยค้นหา bug ออกจาก TypeScript ที่เพียงแค่เพิ่ม syntax เข้าไปเท่านั้น