ข้ามไปเนื้อหาหลัก

Category: reference

TypeScript Type Narrowing — จัดการ Union Types อย่างปลอดภัย

เทคนิค TypeScript type narrowing ที่ช่วยให้ compiler รู้ type ที่แน่นอน ณ จุดนั้น ครอบคลุม typeof, instanceof, in, discriminated unions และ type guards

· อ่านประมาณ 3 นาที

สารบัญ

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);