模板字面量类型(Template Literal Types)让我们能在类型层面操作字符串,类似于 JavaScript 的模板字符串。
基本语法#
使用反引号创建模板字面量类型:
// TypeScript 5.x
// 简单拼接type Greeting = `hello ${string}`
const a: Greeting = 'hello world' // ✅const b: Greeting = 'hello there' // ✅// const c: Greeting = 'hi world'; // ❌ 错误
// 使用字面量类型type World = 'world'type HelloWorld = `hello ${World}`// 'hello world'
// 联合类型展开type Color = 'red' | 'blue' | 'green'type Size = 'small' | 'medium' | 'large'
type ColorSize = `${Color}-${Size}`// 'red-small' | 'red-medium' | 'red-large'// | 'blue-small' | 'blue-medium' | 'blue-large'// | 'green-small' | 'green-medium' | 'green-large'
// 数字类型type Pixel = `${number}px`
const width: Pixel = '100px' // ✅// const height: Pixel = '100'; // ❌ 错误内置字符串工具类型#
TypeScript 提供了四个内置的字符串操作类型:
// Uppercase - 转大写type Upper = Uppercase<'hello'>// 'HELLO'
type UpperUnion = Uppercase<'hello' | 'world'>// 'HELLO' | 'WORLD'
// Lowercase - 转小写type Lower = Lowercase<'HELLO'>// 'hello'
// Capitalize - 首字母大写type Cap = Capitalize<'hello'>// 'Hello'
type CapUnion = Capitalize<'hello' | 'world'>// 'Hello' | 'World'
// Uncapitalize - 首字母小写type Uncap = Uncapitalize<'Hello'>// 'hello'
// 组合使用type EventName<T extends string> = `on${Capitalize<T>}`
type ClickEvent = EventName<'click'>// 'onClick'
type FocusEvent = EventName<'focus'>// 'onFocus'字符串模式匹配#
使用 infer 进行字符串模式匹配:
// 提取前缀后的部分type RemovePrefix< T extends string, P extends string,> = T extends `${P}${infer Rest}` ? Rest : T
type Without = RemovePrefix<'hello_world', 'hello_'>// 'world'
type NoMatch = RemovePrefix<'hello_world', 'hi_'>// 'hello_world'
// 提取后缀前的部分type RemoveSuffix< T extends string, S extends string,> = T extends `${infer Rest}${S}` ? Rest : T
type WithoutSuffix = RemoveSuffix<'hello.ts', '.ts'>// 'hello'
// 替换字符串type Replace< T extends string, S extends string, R extends string,> = T extends `${infer Before}${S}${infer After}` ? `${Before}${R}${After}` : T
type Replaced = Replace<'hello world', ' ', '-'>// 'hello-world'
// 全部替换type ReplaceAll< T extends string, S extends string, R extends string,> = T extends `${infer Before}${S}${infer After}` ? ReplaceAll<`${Before}${R}${After}`, S, R> : T
type ReplacedAll = ReplaceAll<'a.b.c', '.', '/'>// 'a/b/c'字符串分割#
// 分割字符串为元组type Split< S extends string, D extends string,> = S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : S extends '' ? [] : [S]
type Parts = Split<'a-b-c', '-'>// ['a', 'b', 'c']
type Words = Split<'hello world', ' '>// ['hello', 'world']
// 连接字符串数组type Join<T extends string[], D extends string> = T extends [ infer First extends string,] ? First : T extends [infer First extends string, ...infer Rest extends string[]] ? `${First}${D}${Join<Rest, D>}` : ''
type Joined = Join<['a', 'b', 'c'], '-'>// 'a-b-c'命名转换#
// 驼峰转短横线type CamelToKebab<S extends string> = S extends `${infer First}${infer Rest}` ? First extends Uppercase<First> ? `-${Lowercase<First>}${CamelToKebab<Rest>}` : `${First}${CamelToKebab<Rest>}` : S
type Kebab = CamelToKebab<'backgroundColor'>// 'background-color'
// 短横线转驼峰type KebabToCamel<S extends string> = S extends `${infer First}-${infer Rest}` ? `${First}${Capitalize<KebabToCamel<Rest>>}` : S
type Camel = KebabToCamel<'background-color'>// 'backgroundColor'
// 蛇形转驼峰type SnakeToCamel<S extends string> = S extends `${infer First}_${infer Rest}` ? `${First}${Capitalize<SnakeToCamel<Rest>>}` : S
type FromSnake = SnakeToCamel<'user_first_name'>// 'userFirstName'
// 驼峰转蛇形type CamelToSnake<S extends string> = S extends `${infer First}${infer Rest}` ? First extends Uppercase<First> ? `_${Lowercase<First>}${CamelToSnake<Rest>}` : `${First}${CamelToSnake<Rest>}` : S
type ToSnake = CamelToSnake<'userFirstName'>// 'user_first_name'实际应用#
类型安全的事件系统#
type Events = { click: { x: number; y: number } focus: { target: HTMLElement } blur: { target: HTMLElement }}
type EventHandler<T extends keyof Events> = `on${Capitalize<T>}`
type AllHandlers = { [K in keyof Events as EventHandler<K>]: (event: Events[K]) => void}// {// onClick: (event: { x: number; y: number }) => void// onFocus: (event: { target: HTMLElement }) => void// onBlur: (event: { target: HTMLElement }) => void// }Getter/Setter 生成#
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}
interface State { name: string count: number loading: boolean}
type StateGetters = Getters<State>// {// getName: () => string// getCount: () => number// getLoading: () => boolean// }
type StateSetters = Setters<State>// {// setName: (value: string) => void// setCount: (value: number) => void// setLoading: (value: boolean) => void// }
type StateWithMethods = State & Getters<State> & Setters<State>CSS 属性类型#
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'type CSSValue = `${number}${CSSUnit}`
type Spacing = CSSValue | 'auto'
interface CSSProperties { margin?: Spacing padding?: Spacing width?: CSSValue | 'auto' | '100%' height?: CSSValue | 'auto' | '100%'}
const styles: CSSProperties = { margin: '10px', padding: '1.5rem', width: '100%', height: 'auto',}
// 颜色类型type HexColor = `#${string}`type RGBColor = `rgb(${number}, ${number}, ${number})`type RGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`
type Color = HexColor | RGBColor | RGBAColor | 'transparent'
const color1: Color = '#ff0000'const color2: Color = 'rgb(255, 0, 0)'const color3: Color = 'rgba(255, 0, 0, 0.5)'路由类型#
type PathParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}` ? Param | PathParams<`/${Rest}`> : Path extends `${string}:${infer Param}` ? Param : never
type UserParams = PathParams<'/users/:id'>// 'id'
type PostParams = PathParams<'/users/:userId/posts/:postId'>// 'userId' | 'postId'
// 构建参数对象type ParamsObject<Path extends string> = { [K in PathParams<Path>]: string}
type UserParamsObj = ParamsObject<'/users/:id'>// { id: string }
type PostParamsObj = ParamsObject<'/users/:userId/posts/:postId'>// { userId: string; postId: string }
// 路由配置interface Route<Path extends string> { path: Path handler: (params: ParamsObject<Path>) => void}
const userRoute: Route<'/users/:id'> = { path: '/users/:id', handler: (params) => { console.log(params.id) // 类型安全 },}数据库查询构建器#
type Operator = 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte'
type WhereClause<T, K extends keyof T> = `${K & string}_${Operator}`
interface User { id: number name: string age: number email: string}
type UserWhere = { [K in keyof User as WhereClause<User, K>]?: User[K]}// {// id_eq?: number; id_ne?: number; id_gt?: number; ...// name_eq?: string; name_ne?: string; ...// age_eq?: number; ...// email_eq?: string; ...// }
const query: UserWhere = { age_gte: 18, name_eq: '张三',}国际化键#
type I18nKey<T extends string> = `i18n.${T}`
type Namespace = 'common' | 'auth' | 'dashboard'type Key = 'title' | 'description' | 'button'
type AllKeys = I18nKey<`${Namespace}.${Key}`>// 'i18n.common.title' | 'i18n.common.description' | ...
// 嵌套键type NestedKey<T, Prefix extends string = ''> = T extends object ? { [K in keyof T]: K extends string ? T[K] extends object ? NestedKey<T[K], `${Prefix}${K}.`> : `${Prefix}${K}` : never }[keyof T] : never
interface Translations { common: { title: string button: { submit: string cancel: string } } auth: { login: string logout: string }}
type TranslationKeys = NestedKey<Translations>// 'common.title' | 'common.button.submit' | 'common.button.cancel' | 'auth.login' | 'auth.logout'高级技巧#
字符串长度#
type StringLength< S extends string, Acc extends any[] = [],> = S extends `${infer _}${infer Rest}` ? StringLength<Rest, [...Acc, any]> : Acc['length']
type Len1 = StringLength<'hello'>// 5
type Len2 = StringLength<''>// 0字符串反转#
type Reverse<S extends string> = S extends `${infer First}${infer Rest}` ? `${Reverse<Rest>}${First}` : ''
type Rev = Reverse<'hello'>// 'olleh'去除空格#
type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S
type TrimRight<S extends string> = S extends `${infer Rest} ` ? TrimRight<Rest> : S
type Trim<S extends string> = TrimLeft<TrimRight<S>>
type Trimmed = Trim<' hello '>// 'hello'常见问题#
🙋 模板字面量有性能限制吗?#
// 联合类型会产生笛卡尔积type Many = `${1 | 2 | 3}${1 | 2 | 3}${1 | 2 | 3}`// 产生 27 种组合
// 过多组合会导致编译变慢// 避免过度复杂的模板字面量类型🙋 如何处理动态字符串?#
// 使用 string 类型匹配任意字符串type HasPrefix<T extends string, P extends string> = T extends `${P}${string}` ? true : false
type Test1 = HasPrefix<'hello-world', 'hello'>// true
type Test2 = HasPrefix<'hello-world', 'world'>// false🙋 模板字面量可以用于运行时吗?#
// 模板字面量类型只存在于编译时// 运行时需要使用类型守卫验证
function isValidEvent(event: string): event is `on${Capitalize<string>}` { return event.startsWith('on') && event.length > 2}
const event = 'onClick'if (isValidEvent(event)) { console.log(event) // 类型为 `on${Capitalize<string>}`}总结#
| 特性 | 语法 | 用途 |
|---|---|---|
| 基本拼接 | `${A}${B}` | 组合字符串类型 |
| 联合展开 | `${A | B}` | 生成所有组合 |
| Uppercase | Uppercase<T> | 转大写 |
| Lowercase | Lowercase<T> | 转小写 |
| Capitalize | Capitalize<T> | 首字母大写 |
| Uncapitalize | Uncapitalize<T> | 首字母小写 |
| 模式匹配 | `${infer X}...` | 提取字符串部分 |
下一篇我们将学习模块系统,了解 TypeScript 的模块化方案。