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