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

Category: reference

TypeScript satisfies & const Assertions

satisfies operator (TS 4.9) และ as const ช่วย infer types ที่แคบลงโดยไม่ต้อง annotate — ทำให้โค้ดปลอดภัยขึ้นโดยไม่เสีย flexibility

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

สารบัญ

satisfies operator (TypeScript 4.9+)

ปัญหาของ type annotation ปกติ: ทำให้ type กว้างขึ้น ทำให้เสีย narrowing

// ❌ annotate แบบ type annotation — type กว้างขึ้น
const palette: Record<string, string | string[]> = {
  red:   '#ef4444',
  green: ['#4ade80', '#16a34a'],
  blue:  '#3b82f6',
};

palette.red.toUpperCase();  // ❌ Error: string | string[] ไม่มี toUpperCase

// ✓ satisfies — validate กับ type แต่ยัง infer type ที่แคบกว่า
const palette = {
  red:   '#ef4444',
  green: ['#4ade80', '#16a34a'],
  blue:  '#3b82f6',
} satisfies Record<string, string | string[]>;

palette.red.toUpperCase();    // ✓ TypeScript รู้ว่า red เป็น string
palette.green.includes('#4ade80');  // ✓ รู้ว่า green เป็น string[]

satisfies vs Type Annotation vs as

type Config = { port: number; host: string };

// 1. Type annotation — lose narrowing
const a: Config = { port: 3000, host: 'localhost' };
a.port;  // number

// 2. satisfies — validate + keep narrow type
const b = { port: 3000, host: 'localhost' } satisfies Config;
b.port;  // 3000 (literal type ไม่ถูก widen)

// 3. as Config — unsafe cast, ไม่มี validation
const c = { port: 3000, host: 'localhost' } as Config;
// ❌ ไม่ validate: { port: 3000 } as Config จะผ่านแม้ขาด host

Use Cases ของ satisfies

1. Object ที่มีค่าหลาย types

type Routes = Record<string, { path: string; auth?: boolean }>;

const routes = {
  home:    { path: '/' },
  profile: { path: '/profile', auth: true },
  login:   { path: '/login' },
} satisfies Routes;

// ✓ รู้ว่า auth เป็น true ไม่ใช่ boolean | undefined สำหรับ profile
routes.profile.auth;  // true

2. Enum-like objects

type Status = 'active' | 'completed' | 'archived';
type StatusConfig = Record<Status, { label: string; color: string }>;

const STATUS = {
  active:    { label: 'กำลังทำ', color: '#10b981' },
  completed: { label: 'เสร็จแล้ว', color: '#3b82f6' },
  archived:  { label: 'เก็บไว้', color: '#94a3b8' },
} satisfies StatusConfig;

// ✓ TypeScript error ถ้าลืม key หรือใส่ key ผิด
// ✓ ยัง infer type ของ value ได้อย่างแม่นยำ

as const — Const Assertions

// ❌ ไม่มี as const: TypeScript widen ค่า
const direction = 'left';         // type: string
const directions = ['left', 'right'];  // type: string[]
const config = { host: 'localhost', port: 3000 };  // { host: string; port: number }

// ✓ as const: freeze ทุกอย่างเป็น readonly literal
const direction = 'left' as const;   // type: 'left'
const directions = ['left', 'right'] as const;  // type: readonly ['left', 'right']
const config = { host: 'localhost', port: 3000 } as const;
// type: { readonly host: 'localhost'; readonly port: 3000 }

as const กับ Union Types

// สร้าง union type จาก array
const SIZES = ['sm', 'md', 'lg', 'xl'] as const;
type Size = (typeof SIZES)[number];  // 'sm' | 'md' | 'lg' | 'xl'

function resize(size: Size) { /* ... */ }
resize('md');     // ✓
resize('xxl');    // ❌ TypeScript error

// สร้าง object keys เป็น union
const HTTP_METHODS = {
  GET: 'GET',
  POST: 'POST',
  PUT: 'PUT',
  DELETE: 'DELETE',
} as const;
type HttpMethod = (typeof HTTP_METHODS)[keyof typeof HTTP_METHODS];
// 'GET' | 'POST' | 'PUT' | 'DELETE'

ใช้คู่กัน: satisfies + as const

type NavItem = { label: string; href: string; icon?: string };

const NAV = [
  { label: 'Projects', href: '/projects', icon: 'folder' },
  { label: 'Resources', href: '/resources', icon: 'book' },
  { label: 'About', href: '/about' },
] as const satisfies readonly NavItem[];

// ✓ validate ว่า match NavItem[]
// ✓ tuple type — รู้ว่า index 0 มี icon: 'folder' ไม่ใช่ string | undefined
NAV[0].icon;    // 'folder' (literal)
NAV[2].href;    // '/about' (literal)

const กับ Generic Functions

// ✓ ใช้ const type parameter (TS 5.0+)
function createRoute<const T extends string>(path: T) {
  return { path, match: (url: string) => url === path };
}

const homeRoute = createRoute('/home');
homeRoute.path;  // '/home' (literal) ไม่ใช่ string

เปรียบเทียบ

type annotationas Typesatisfies Typeas const
Validate
Keep narrow❌ widen❌ widen
Readonly
ใช้เมื่อexplicit typecastvalidate + narrowfreeze literals