Category: reference
TypeScript Type Narrowing — จัดการ Union Types อย่างปลอดภัย
เทคนิค TypeScript type narrowing ที่ช่วยให้ compiler รู้ type ที่แน่นอน ณ จุดนั้น ครอบคลุม typeof, instanceof, in, discriminated unions และ type guards
สารบัญ
Type Narrowing คืออะไร
TypeScript ติดตาม type ของตัวแปร ณ แต่ละจุดในโค้ด เมื่อเจอ condition ที่ตรวจ type TypeScript จะ “narrow” (จำกัด) type ให้แคบลง:
function greet(name: string | null) {
// ตรงนี้ name เป็น string | null
if (name === null) {
// ตรงนี้ TypeScript รู้ว่า name เป็น null
return 'Hello, anonymous!';
}
// ตรงนี้ name เป็น string แน่นอน (null ถูก narrow ออกไปแล้ว)
return `Hello, ${name.toUpperCase()}!`;
}
typeof Guard
function formatValue(value: string | number | boolean) {
if (typeof value === 'string') {
return value.trim(); // string methods ใช้ได้
}
if (typeof value === 'number') {
return value.toFixed(2); // number methods ใช้ได้
}
return String(value); // boolean เหลืออยู่
}
instanceof Guard
class ProjectError extends Error {
constructor(public projectId: string, message: string) {
super(message);
}
}
function handleError(err: unknown) {
if (err instanceof ProjectError) {
console.error(`Project ${err.projectId}: ${err.message}`);
} else if (err instanceof Error) {
console.error(err.message);
} else {
console.error('Unknown error');
}
}
in Operator
ตรวจว่า property มีอยู่ใน object หรือไม่:
type Project = { type: 'project'; status: string; url?: string };
type Resource = { type: 'resource'; category: string };
function getInfo(item: Project | Resource) {
if ('status' in item) {
return item.status; // item เป็น Project
}
return item.category; // item เป็น Resource
}
Discriminated Unions (แนะนำมากที่สุด)
เพิ่ม property เดียวที่บอก type ได้แน่นอน:
type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success'; data: string[] };
type ErrorState = { status: 'error'; message: string };
type State = LoadingState | SuccessState | ErrorState;
function render(state: State) {
switch (state.status) {
case 'loading':
return '<Spinner />';
case 'success':
return state.data.join(', '); // data มีอยู่แน่นอน
case 'error':
return state.message; // message มีอยู่แน่นอน
}
}
TypeScript ตรวจได้ว่า switch ครอบคลุมทุก case แล้ว
Type Guards (Custom)
// ประกาศ function ที่ return type predicate
function isProject(item: unknown): item is { title: string; status: string } {
return (
typeof item === 'object' &&
item !== null &&
'title' in item &&
'status' in item
);
}
function processItem(item: unknown) {
if (isProject(item)) {
console.log(item.title); // TypeScript รู้ว่า item.title มีอยู่
}
}
Exhaustiveness Check
type Direction = 'north' | 'south' | 'east' | 'west';
function move(dir: Direction) {
switch (dir) {
case 'north': return 'moving north';
case 'south': return 'moving south';
case 'east': return 'moving east';
case 'west': return 'moving west';
default:
// ถ้าเพิ่ม direction ใหม่ใน type แต่ลืมเพิ่ม case
// TypeScript จะ error ที่บรรทัดนี้
const _exhaustive: never = dir;
throw new Error(`Unknown direction: ${_exhaustive}`);
}
}
Nullish Coalescing และ Optional Chaining
const user: { name?: string; address?: { city?: string } } = {};
// Optional chaining — return undefined ถ้า property ไม่มี
const city = user.address?.city;
// Nullish coalescing — fallback เมื่อ null หรือ undefined
const name = user.name ?? 'anonymous';
// ผสมกัน
const displayCity = user.address?.city ?? 'ไม่ระบุเมือง';
Assertion Functions
function assertDefined<T>(val: T | null | undefined, msg: string): asserts val is T {
if (val == null) throw new Error(msg);
}
const slug = Astro.params.slug;
assertDefined(slug, 'slug is required');
// หลังบรรทัดนี้ TypeScript รู้ว่า slug เป็น string แน่นอน
const post = await getEntry('posts', slug);
ใช้กับ Astro Content Collections
// getEntry อาจ return undefined ถ้าไม่เจอ
const project = await getEntry('projects', slug);
// แบบง่าย
if (!project) throw new Error(`Project not found: ${slug}`);
// หรือใช้ assertion
assertDefined(project, `Project not found: ${slug}`);
// หลังจากนี้ project เป็น CollectionEntry แน่นอน
const { Content } = await render(project);