TypeScript 模版字面量型別 (Template Literal Types)
模版字面量型別 (Template Literal Types) 是 TypeScript 4.1 引入的功能,它結合了字面量型別和模版字串的能力,允許我們在型別層級進行字串操作。
基本語法
使用反引號和 ${} 語法:
type Greeting = `Hello, ${string}`;
let a: Greeting = 'Hello, World'; // OK
let b: Greeting = 'Hello, TypeScript'; // OK
// let c: Greeting = "Hi, World"; // 錯誤
與字面量型別結合
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "green-small" | "green-medium" | "green-large" |
// "blue-small" | "blue-medium" | "blue-large"
let item: ColoredSize = 'red-small'; // OK
let item2: ColoredSize = 'green-large'; // OK
// let item3: ColoredSize = "yellow-small"; // 錯誤
內建字串操作型別
TypeScript 提供了四個內建的字串操作型別:
Uppercase<S>
將字串轉換為大寫:
type Loud = Uppercase<'hello'>; // "HELLO"
type Event = 'click' | 'focus';
type UpperEvent = Uppercase<Event>; // "CLICK" | "FOCUS"
Lowercase<S>
將字串轉換為小寫:
type Quiet = Lowercase<'HELLO'>; // "hello"
type Command = 'START' | 'STOP';
type LowerCommand = Lowercase<Command>; // "start" | "stop"
Capitalize<S>
將首字母大寫:
type Title = Capitalize<'hello'>; // "Hello"
type Property = 'name' | 'age';
type GetterName = `get${Capitalize<Property>}`; // "getName" | "getAge"
Uncapitalize<S>
將首字母小寫:
type Lower = Uncapitalize<'Hello'>; // "hello"
type Method = 'GetUser' | 'SetUser';
type CamelMethod = Uncapitalize<Method>; // "getUser" | "setUser"
事件處理器模式
type EventName = 'click' | 'focus' | 'blur' | 'change';
type EventHandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange"
type EventHandler = (event: Event) => void;
type EventHandlers = {
[K in EventName as `on${Capitalize<K>}`]: EventHandler;
};
// {
// onClick: EventHandler;
// onFocus: EventHandler;
// onBlur: EventHandler;
// onChange: EventHandler;
// }
Getter 和 Setter 模式
interface User {
name: string;
age: number;
email: string;
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
};
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
type UserSetters = Setters<User>;
// {
// setName: (value: string) => void;
// setAge: (value: number) => void;
// setEmail: (value: string) => void;
// }
type UserAccessors = Getters<User> & Setters<User>;
CSS 屬性型別
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;
let width: CSSValue = '100px'; // OK
let height: CSSValue = '50vh'; // OK
let margin: CSSValue = '1.5rem'; // OK
// let padding: CSSValue = "10"; // 錯誤
type CSSColor = `#${string}` | `rgb(${number}, ${number}, ${number})`;
let color1: CSSColor = '#ff0000'; // OK
let color2: CSSColor = 'rgb(255, 0, 0)'; // OK
API 路由型別
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type ApiEndpoint = `/${ApiVersion}/${Resource}`;
// "/v1/users" | "/v1/posts" | "/v1/comments" |
// "/v2/users" | "/v2/posts" | "/v2/comments"
type ApiRoute = `${HttpMethod} ${ApiEndpoint}`;
// "GET /v1/users" | "POST /v1/users" | ...
// 帶參數的路由
type ResourceId = `/${Resource}/:id`;
// "/users/:id" | "/posts/:id" | "/comments/:id"
type NestedResource = `/${Resource}/:id/${Resource}`;
// "/users/:id/users" | "/users/:id/posts" | ...
提取字串部分
結合條件型別和 infer:
// 提取前綴後的部分
type RemovePrefix<S extends string, P extends string> = S extends `${P}${infer Rest}`
? Rest
: never;
type WithoutGet = RemovePrefix<'getName', 'get'>; // "Name"
type WithoutOn = RemovePrefix<'onClick', 'on'>; // "Click"
// 提取後綴前的部分
type RemoveSuffix<S extends string, Suffix extends string> = S extends `${infer Rest}${Suffix}`
? Rest
: never;
type WithoutId = RemoveSuffix<'userId', 'Id'>; // "user"
分割字串
type Split<S extends string, D extends string> = S extends `${infer T}${D}${infer U}`
? [T, ...Split<U, D>]
: [S];
type Parts = Split<'a-b-c', '-'>; // ["a", "b", "c"]
type Words = Split<'hello world', ' '>; // ["hello", "world"]
路徑參數提取
type ExtractParams<Path extends string> =
Path extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: Path extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// "userId" | "postId"
字串轉換
駝峰式轉連字號
type CamelToKebab<S extends string> = S extends `${infer C}${infer Rest}`
? C extends Uppercase<C>
? `-${Lowercase<C>}${CamelToKebab<Rest>}`
: `${C}${CamelToKebab<Rest>}`
: S;
type Kebab = CamelToKebab<'backgroundColor'>; // "background-color"
type Kebab2 = CamelToKebab<'fontSize'>; // "font-size"
連字號轉駝峰式
type KebabToCamel<S extends string> = S extends `${infer Start}-${infer Rest}`
? `${Start}${KebabToCamel<Capitalize<Rest>>}`
: S;
type Camel = KebabToCamel<'background-color'>; // "backgroundColor"
type Camel2 = KebabToCamel<'font-size'>; // "fontSize"
實用範例
i18n 鍵值型別
type Locale = 'en' | 'zh' | 'ja';
type Namespace = 'common' | 'home' | 'about';
type I18nKey = `${Namespace}.${string}`;
type LocalizedKey = `${Locale}:${I18nKey}`;
function translate(key: I18nKey): string {
// ...
return '';
}
translate('common.hello'); // OK
translate('home.title'); // OK
// translate("invalid"); // 錯誤
環境變數型別
type EnvPrefix = 'NEXT_PUBLIC_' | 'REACT_APP_' | 'VITE_';
type EnvVar = `${EnvPrefix}${string}`;
type PublicEnv = `NEXT_PUBLIC_${string}`;
type ApiUrl = `${PublicEnv}API_URL`;
const apiUrl: ApiUrl = 'NEXT_PUBLIC_API_URL';
資料庫查詢
type TableName = 'users' | 'posts' | 'comments';
type Column<T extends TableName> = T extends 'users'
? 'id' | 'name' | 'email'
: T extends 'posts'
? 'id' | 'title' | 'content' | 'author_id'
: T extends 'comments'
? 'id' | 'text' | 'post_id' | 'user_id'
: never;
type SelectQuery<T extends TableName> = `SELECT ${Column<T> | '*'} FROM ${T}`;
type UserQuery = SelectQuery<'users'>;
// "SELECT id FROM users" | "SELECT name FROM users" | ...
type WhereClause<T extends TableName> = `WHERE ${Column<T>} = ?`;
type FullQuery<T extends TableName> = `${SelectQuery<T>} ${WhereClause<T>}`;
Redux Action 型別
type EntityName = 'user' | 'post' | 'comment';
type ActionVerb = 'fetch' | 'create' | 'update' | 'delete';
type ActionType = `${EntityName}/${ActionVerb}`;
// "user/fetch" | "user/create" | "user/update" | "user/delete" |
// "post/fetch" | "post/create" | ...
type AsyncActionType =
| `${ActionType}`
| `${ActionType}/pending`
| `${ActionType}/fulfilled`
| `${ActionType}/rejected`;
interface Action<T extends AsyncActionType> {
type: T;
payload?: unknown;
}
效能考量
模版字面量型別與聯合型別結合時,可能會產生大量的組合:
// 這會產生很多組合!
type A = 'a' | 'b' | 'c' | 'd' | 'e'; // 5 個
type B = '1' | '2' | '3' | '4' | '5'; // 5 個
type Combined = `${A}${B}`; // 25 個組合
// 更多層級會指數增長
type Triple = `${A}${B}${A}`; // 125 個組合
當聯合型別過大時,可能會影響編譯效能。建議在必要時才使用,並注意組合數量。