เรื่อง any vs unknown ใน TypeScript สรุปได้ด้วยคำถามเดียว คือ compiler ยังตรวจสิ่งที่คุณทำกับค่านั้นอยู่หรือไม่ ทั้งสอง type รับค่าอะไรก็ได้เหมือนกัน แต่ any ปิดการตรวจ type ไปเลย ทุกการเข้าถึง property และทุกการเรียกเมธอดจะ compile ผ่าน แม้แต่อันที่พังตอน runtime ส่วน unknown บังคับให้คุณพิสูจน์ก่อนว่าค่านั้นเป็นอะไร ด้วยการเช็กอย่าง typeof หรือ instanceof แล้วจึงใช้งานได้ ค่าที่ยังไม่รู้ type ควรใช้ unknown
ความต่างนี้ตั้งอยู่บนพื้นฐานอีกไม่กี่เรื่องที่ควรเข้าใจให้ถูกตั้งแต่ต้น ได้แก่ primitive type และ literal type, ผลของ strictNullChecks ต่อ null และ undefined, ความต่างระหว่าง void กับ never และจังหวะที่ควรใช้ type assertion กับ as const บทความนี้อธิบายทีละเรื่องพร้อมตัวอย่างบน TypeScript 5.x
Primitive type ใน TypeScript
TypeScript มี primitive ชุดเดียวกับ JavaScript:
| Type | ตัวอย่างค่า | หมายเหตุ |
|---|---|---|
string | "hello", `id-${n}` | |
number | 42, 3.14, NaN | จำนวนเต็มและทศนิยมใช้ type เดียวกัน |
bigint | 9007199254740993n | ต้องตั้ง target เป็น ES2020 ขึ้นไป |
boolean | true, false | |
symbol | Symbol("id") | ใช้เป็น key ที่ไม่ซ้ำ |
null | null | ตั้งใจให้ไม่มีค่า |
undefined | undefined | ยังไม่ได้กำหนดค่า |
ให้ใช้ชื่อตัวพิมพ์เล็ก String, Number และ Boolean เป็น type ของ wrapper object ซึ่งแทบไม่เคยเป็นสิ่งที่คุณต้องการ
ตัวแปร local ส่วนใหญ่ไม่ต้องใส่ annotation เพราะ TypeScript infer ให้เอง สิ่งที่ควรระบุคือ parameter ของฟังก์ชัน, return type ของฟังก์ชันที่ export และตัวแปรที่เริ่มต้นด้วยค่าว่าง:
let count = 0; // inferred as number
const ids: string[] = []; // empty array needs an annotation
function total(prices: number[]): number {
return prices.reduce((sum, p) => sum + p, 0);
}Literal type: let กับ const infer ต่างกันอย่างไร
Literal type คือ type ที่มีค่าได้ค่าเดียว เช่น "GET" หรือ 404 keyword ที่ใช้ประกาศตัวแปรเป็นตัวตัดสินว่า TypeScript จะเก็บ literal ไว้หรือขยาย (widen) เป็น type กว้าง:
let a = "GET"; // string: a let can be reassigned
const b = "GET"; // "GET": a const can never change
var c = 404; // numberLiteral type จะมีประโยชน์จริงเมื่ออยู่ใน union ซึ่งทำหน้าที่เหมือน enum แบบเบา ๆ:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
function request(url: string, method: HttpMethod) { /* ... */ }
request("/users", "GET"); // OK
request("/users", "FETCH"); // Error: '"FETCH"' is not assignable to parameter of type 'HttpMethod'Union ของ literal รวมถึง discriminated union และ template literal type มีอธิบายละเอียดใน TypeScript advanced types
strictNullChecks: ทำให้ null และ undefined ชัดเจน
ถ้าไม่เปิด strictNullChecks ค่า null และ undefined จะ assign ให้ type ไหนก็ได้ string จึงแอบหมายถึง "string หรืออาจไม่มีอะไรเลย" พอเปิดแล้ว คุณต้องเขียนให้ชัดเอง:
{
"compilerOptions": {
"strict": true
}
}"strict": true เปิด strictNullChecks พร้อมกับ noImplicitAny และการตรวจอื่น ๆ อีกหลายตัว ควรเปิดในทุกโปรเจกต์ใหม่ ดูการตั้งค่า tsconfig.json แบบครบได้ใน คู่มือตั้งค่าโปรเจกต์ TypeScript
เมื่อเปิด strict null checks แล้ว compiler จะจับ crash คลาสสิกแบบ "cannot read properties of undefined" ได้ตั้งแต่ตอนเขียน:
interface User { id: number; name: string }
function findUser(id: number): User | undefined {
return users.find((u) => u.id === id);
}
const user = findUser(1);
console.log(user.name); // Error: 'user' is possibly 'undefined'
console.log(user?.name); // OK: string | undefined
const name = user?.name ?? "Guest"; // OK: stringOptional chaining (?.) จะหยุดเมื่อเจอ null หรือ undefined และ nullish coalescing (??) ใช้ใส่ค่า fallback ส่วน non-null assertion user!.name ก็ compile ผ่านเช่นกัน แต่มันแค่ปิด error ไว้ ใช้เฉพาะตอนที่คุณรู้บางอย่างที่ compiler ไม่รู้จริง ๆ
any vs unknown ใน TypeScript
ทั้งสอง type รับค่าอะไรก็ได้ ความต่างอยู่ที่สิ่งที่ทำได้กับค่านั้นหลังจากรับมาแล้ว
let a: any = "hello";
a.toFixed(2); // compiles, then throws: a.toFixed is not a function
const n: number = a; // compiles: any is assignable to everything
let u: unknown = "hello";
u.toFixed(2); // Error: 'u' is of type 'unknown'
const m: number = u; // Error: 'unknown' is not assignable to 'number'
if (typeof u === "string") {
console.log(u.toUpperCase()); // OK: narrowed to string
}any | unknown | |
|---|---|---|
| รับค่าได้ทุกแบบ | ได้ | ได้ |
| เข้าถึง property และเรียกเมธอด | ทำได้ ไม่มีการตรวจ | error จนกว่าจะ narrow |
| Assign ให้ type อื่น | ได้ทุก type | ได้แค่ unknown และ any |
| ลามไปทั่วโค้ด | ลาม ผลลัพธ์ก็เป็น any ด้วย | ไม่ลาม |
| ใช้เมื่อไร | ย้ายโค้ดจาก JavaScript, ทางหนีฉุกเฉิน | input จากภายนอก: JSON, API response, error ใน catch |
การลามคืออันตรายที่แท้จริงของ any เพราะ a.user.profile ก็เป็น any และทุกอย่างที่คำนวณต่อจากมันก็เป็น any เช่นกัน any ตัวเดียวจึงปิดการตรวจ type ในจุดที่ห่างออกไปไกลได้โดยไม่มีใครรู้ตัว
Narrow ค่า unknown อย่างปลอดภัย
การทำให้ unknown ใช้งานได้คือการ narrow มัน ด้วย typeof, instanceof, Array.isArray, operator in หรือฟังก์ชัน type guard
JSON.parse คืนค่าเป็น any นิสัยที่ดีคือรับผลลัพธ์เป็น unknown แล้ว validate ก่อนใช้:
interface Config {
port: number;
host: string;
}
function isConfig(value: unknown): value is Config {
return (
typeof value === "object" &&
value !== null &&
"port" in value && typeof value.port === "number" &&
"host" in value && typeof value.host === "string"
);
}
const raw: unknown = JSON.parse(text);
if (!isConfig(raw)) {
throw new Error("Invalid config file");
}
console.log(raw.port); // raw is Config hereถ้า payload ใหญ่ขึ้น library สำหรับ schema อย่าง Zod หรือ Valibot ทำงานเดียวกันได้ด้วยโค้ดที่น้อยกว่า ส่วน type guard และเทคนิค narrowing อื่น ๆ อยู่ในบทความ interface vs type alias
อีกกรณีที่เจอบ่อยคือ error ใน catch เมื่อเปิด strict ออปชัน useUnknownInCatchVariables จะให้ค่าที่ catch ได้เป็น unknown เพราะ JavaScript ยอมให้ throw อะไรก็ได้:
try {
await saveOrder(order);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error(message);
}เมื่อไรที่ใช้ any ได้
any ยังมีที่ใช้อยู่ เช่น ตอนค่อย ๆ ย้าย codebase จาก JavaScript หรือตอนต้องอ้อม type ของ third-party ที่ผิด ให้จำกัดวงไว้แคบ ๆ เขียน comment บอกเหตุผล และเปิด lint rule @typescript-eslint/no-explicit-any เพื่อให้ any ตัวใหม่ต้องผ่านการ review
void vs never
ทั้งสองใช้กับฟังก์ชันที่ไม่ได้ให้ค่าที่ใช้ประโยชน์ได้ แต่ด้วยเหตุผลต่างกัน
voidหมายถึงฟังก์ชัน return กลับมา แต่ไม่ควรนำค่าที่ return ไปใช้ ตอน runtime มันคืนundefinedneverหมายถึงฟังก์ชันไม่เคย return ตามปกติเลย ต้อง throw หรือวนไม่รู้จบเสมอ
function log(message: string): void {
console.log(message);
}
function fail(message: string): never {
throw new Error(message);
}never ยังเป็น type ของค่าที่เป็นไปไม่ได้ จึงเหมาะกับการตรวจว่าครอบคลุมทุกกรณี (exhaustive check) ถ้ามีคนเพิ่มสมาชิกใหม่ใน union แล้วลืมเพิ่ม case บรรทัดที่ assign ให้ never จะ compile ไม่ผ่าน:
type Status = "active" | "suspended" | "deleted";
function statusLabel(status: Status): string {
switch (status) {
case "active":
return "Active";
case "suspended":
return "Suspended";
case "deleted":
return "Deleted";
default: {
const unreachable: never = status;
throw new Error(`Unhandled status: ${unreachable}`);
}
}
}มีรายละเอียดของ void ที่หลายคนงง คือ function type ที่ return void ยอมรับฟังก์ชันที่ return ค่าได้ นี่คือเหตุผลที่ items.forEach((x) => results.push(x)) compile ผ่าน แม้ push จะคืนค่าเป็น number ค่านั้นแค่ถูกทิ้งไป
Type assertion: บอก compiler ในสิ่งที่คุณรู้
Type assertion ด้วย as บอกให้ TypeScript มองค่าหนึ่งเป็น type ที่เจาะจงขึ้น มันไม่ทำอะไรเลยตอน runtime ไม่มีการแปลงค่าและไม่มีการตรวจ
const input = document.getElementById("email") as HTMLInputElement;
input.value = "[email protected]";getElementById คืนค่า HTMLElement | null การ assert แบบนี้เท่ากับบอกว่า "ฉันรู้ว่านี่คือ input และมันมีอยู่จริง" ถ้าคุณเดาผิด error จะไปโผล่ตอน runtime แทน เวอร์ชันที่ปลอดภัยกว่ายังเก็บการเช็ก null ไว้:
const input = document.querySelector<HTMLInputElement>("#email");
if (input) input.value = "[email protected]";TypeScript ยอมให้ assert ได้เฉพาะระหว่าง type ที่ทับซ้อนกัน "hello" as number จะ error การเขียน value as unknown as Target อ้อมข้อจำกัดนี้ได้ แต่แทบทุกครั้งเป็นสัญญาณว่าควรแก้ที่ตัว type มากกว่า
ถ้าต้องการตรวจว่าค่าตรงกับ type โดยไม่เสีย type ที่ infer ได้อย่างละเอียด ให้ใช้ satisfies:
type Route = { path: string; auth: boolean };
const routes = {
home: { path: "/", auth: false },
admin: { path: "/admin", auth: true },
} satisfies Record<string, Route>;
routes.admin.path; // OK: TypeScript still knows the keys are "home" | "admin"ถ้าใช้ annotation แทน (const routes: Record<string, Route>) ชื่อ key จะหายไป และ routes.anything ก็ compile ผ่าน
Const assertion ด้วย as const
as const สั่งให้ TypeScript infer type ที่แคบที่สุดเท่าที่ทำได้ คือได้ค่า literal แทน string หรือ number และได้ property กับ tuple แบบ readonly แทน object และ array ที่แก้ไขได้
const req = { url: "/users", method: "GET" };
request(req.url, req.method);
// Error: 'string' is not assignable to parameter of type 'HttpMethod'
const req2 = { url: "/users", method: "GET" } as const;
request(req2.url, req2.method); // OK: method is "GET"รูปแบบที่มีประโยชน์ที่สุดคือการสร้าง union type จากรายการค่า ทำให้ array ตอน runtime กับ type ไม่มีทางไม่ตรงกัน:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
function isRole(value: string): value is Role {
return (ROLES as readonly string[]).includes(value);
}Object ที่ใส่ as const ยังเป็นทางเลือกยอดนิยมแทน enum ด้วย
คำถามที่พบบ่อย
ควรใช้ unknown หรือ any?
ใช้ unknown กับค่าที่ยังไม่รู้ type เช่น JSON ที่ parse มา, API response และ error ที่ catch ได้ มันบังคับให้ตรวจก่อนใช้ เก็บ any ไว้สำหรับการย้ายโค้ดและการอ้อมปัญหาเฉพาะจุด
void ต่างจาก undefined อย่างไร?
undefined เป็นทั้งค่าและ type ที่ assign และเช็กได้ ส่วน void ใช้กับ return type และหมายถึง "อย่าใช้ค่าที่ return" ฟังก์ชันที่ return void คืน undefined จริงตอน runtime แต่ type ช่วยสื่อเจตนาให้ชัด
Type assertion คือ type casting หรือเปล่า?
ไม่ใช่ casting ในภาษาอย่าง Java หรือ C# อาจแปลงหรือตรวจค่าตอน runtime แต่ assertion ของ TypeScript ถูกลบทิ้งตอน compile และเปลี่ยนแค่สิ่งที่ compiler เชื่อ
as const ใน TypeScript ทำอะไร?
ทำให้ TypeScript infer เป็น literal type และ property แบบ readonly เช่น ["a", "b"] as const มี type เป็น readonly ["a", "b"] แทน string[]
unknown มีต้นทุนตอน runtime ไหม?
ไม่มี เหมือน type อื่น ๆ ของ TypeScript มันหายไปตอน compile โค้ดที่เหลือตอน runtime มีแค่การเช็กที่คุณเขียนเพื่อ narrow เท่านั้น
Checklist เรื่อง type พื้นฐาน
- เปิด
"strict": trueในทุกโปรเจกต์ - ปล่อยให้ตัวแปร local ใช้ inference ส่วน parameter และ return type ของ public API ให้ระบุเอง
- ใช้ literal union กับชุดค่าที่ตายตัวและมีไม่มาก
- ใช้
unknownกับ input ที่ไม่น่าไว้ใจ แล้ว narrow ด้วย type guard หรือ schema - ใช้
neverตรวจว่าswitchครอบคลุมทุกกรณี - เลือก
satisfiesและการเช็ก null ก่อนจะใช้as - ใช้
as constเพื่อสร้าง type จากค่าตอน runtime
เมื่อพื้นฐานเหล่านี้แน่นแล้ว ขั้นต่อไปคือ TypeScript generics สำหรับเขียนฟังก์ชันที่ใช้ซ้ำได้และยัง type-safe ถ้าทีมของคุณกำลังเริ่ม codebase TypeScript และอยากวางโครงให้ดีตั้งแต่วันแรก Vectorkub ช่วยได้
