TypeScript 字面量型別 (Literal Types)

字面量型別 (Literal Types) 允許我們指定一個變數只能是某個特定的值。這是 TypeScript 型別系統中非常強大的功能,可以讓型別更加精確。

字串字面量型別

// 只能是 "hello" 這個值
let greeting: 'hello' = 'hello';
// greeting = "hi";  // 錯誤:型別 '"hi"' 不可指派給型別 '"hello"'

// 常見用法:聯合字面量型別
type Direction = 'up' | 'down' | 'left' | 'right';

function move(direction: Direction): void {
  console.log(`Moving ${direction}`);
}

move('up'); // OK
move('down'); // OK
// move("forward");  // 錯誤

數字字面量型別

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceRoll {
  return Math.ceil(Math.random() * 6) as DiceRoll;
}

let result: DiceRoll = rollDice();
// result = 7;  // 錯誤:型別 '7' 不可指派給型別 'DiceRoll'

布林字面量型別

type True = true;
type False = false;

let yes: True = true;
// yes = false;  // 錯誤

// 實用場景:根據布林值決定回傳型別
type ApiResult<T, Success extends boolean> = Success extends true
  ? { success: true; data: T }
  : { success: false; error: string };

const 與字面量型別

使用 const 宣告的變數會被推論為字面量型別:

// let 推論為較寬的型別
let message = 'hello'; // 型別是 string

// const 推論為字面量型別
const greeting = 'hello'; // 型別是 "hello"

// 數字也一樣
let num = 42; // 型別是 number
const answer = 42; // 型別是 42

物件中的字面量

物件屬性預設會被推論為較寬的型別:

const config = {
  method: 'GET',
  url: '/api/users',
};
// config.method 的型別是 string,不是 "GET"

// 使用 as const 讓整個物件變成字面量型別
const config2 = {
  method: 'GET',
  url: '/api/users',
} as const;
// config2.method 的型別是 "GET"
// config2 的型別是 { readonly method: "GET"; readonly url: "/api/users" }

as const 斷言

as const 可以將值轉換為最精確的字面量型別:

// 陣列
const colors = ['red', 'green', 'blue'];
// 型別是 string[]

const colors2 = ['red', 'green', 'blue'] as const;
// 型別是 readonly ["red", "green", "blue"]
type Color = (typeof colors2)[number]; // "red" | "green" | "blue"

// 物件
const user = {
  name: '小明',
  role: 'admin',
} as const;
// 型別是 { readonly name: "小明"; readonly role: "admin" }

字面量型別的應用

狀態管理

type Status = 'idle' | 'loading' | 'success' | 'error';

interface State {
  status: Status;
  data: string | null;
  error: string | null;
}

function reducer(
  state: State,
  action: { type: 'LOAD' | 'SUCCESS' | 'ERROR'; payload?: string }
): State {
  switch (action.type) {
    case 'LOAD':
      return { ...state, status: 'loading' };
    case 'SUCCESS':
      return { status: 'success', data: action.payload || null, error: null };
    case 'ERROR':
      return { status: 'error', data: null, error: action.payload || 'Unknown error' };
    default:
      return state;
  }
}

API 方法定義

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

interface RequestConfig {
  method: HttpMethod;
  url: string;
  data?: unknown;
}

function request(config: RequestConfig): Promise<unknown> {
  // ...
}

request({ method: 'GET', url: '/api/users' });
request({ method: 'POST', url: '/api/users', data: { name: '小明' } });
// request({ method: "INVALID", url: "/api" });  // 錯誤

事件類型

type EventType = 'click' | 'focus' | 'blur' | 'change' | 'submit';

function addEventListener(
  element: HTMLElement,
  event: EventType,
  handler: (e: Event) => void
): void {
  element.addEventListener(event, handler);
}

字面量型別與泛型

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = {
  name: '小明',
  age: 25,
  isActive: true,
};

// K 被推論為字面量型別
let name = getProperty(user, 'name'); // string
let age = getProperty(user, 'age'); // number
let active = getProperty(user, 'isActive'); // boolean
// getProperty(user, "invalid");           // 錯誤

模板字面量型別

TypeScript 4.1+ 支援模板字面量型別:

type World = 'world';
type Greeting = `hello ${World}`; // "hello world"

// 組合聯合型別
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" | "blue-small" | ...

// 實用範例:CSS 屬性
type CSSUnit = 'px' | 'em' | 'rem' | '%';
type CSSValue = `${number}${CSSUnit}`;
let width: CSSValue = '100px'; // OK
let height: CSSValue = '50%'; // OK
// let invalid: CSSValue = "abc";  // 錯誤

內建字串操作型別

type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

type Event = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<Event>}`;
// "onClick" | "onFocus" | "onBlur"

型別收窄與字面量

type Result = 'success' | 'error';

function handleResult(result: Result) {
  if (result === 'success') {
    // result 的型別被收窄為 "success"
    console.log('成功!');
  } else {
    // result 的型別被收窄為 "error"
    console.log('失敗!');
  }
}

實用範例

設定檔型別

type Environment = 'development' | 'staging' | 'production';
type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface AppConfig {
  env: Environment;
  logLevel: LogLevel;
  port: number;
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

const config: AppConfig = {
  env: 'development',
  logLevel: 'debug',
  port: 3000,
  features: {
    darkMode: true,
    analytics: false,
  },
};

表單欄位驗證

type FieldType = 'text' | 'email' | 'password' | 'number' | 'date';
type ValidationRule = 'required' | 'minLength' | 'maxLength' | 'pattern';

interface FieldConfig {
  name: string;
  type: FieldType;
  validations: ValidationRule[];
}

const fields: FieldConfig[] = [
  { name: 'username', type: 'text', validations: ['required', 'minLength'] },
  { name: 'email', type: 'email', validations: ['required', 'pattern'] },
  { name: 'password', type: 'password', validations: ['required', 'minLength'] },
];

權限系統

type Permission = 'read' | 'write' | 'delete' | 'admin';
type Role = 'guest' | 'user' | 'editor' | 'admin';

const rolePermissions: Record<Role, Permission[]> = {
  guest: ['read'],
  user: ['read', 'write'],
  editor: ['read', 'write', 'delete'],
  admin: ['read', 'write', 'delete', 'admin'],
};

function hasPermission(role: Role, permission: Permission): boolean {
  return rolePermissions[role].includes(permission);
}

console.log(hasPermission('editor', 'delete')); // true
console.log(hasPermission('user', 'delete')); // false