关于 template-literal-types
这项 TypeScript 技能讲解如何用模板字面量类型描述结构化字符串、解析 DSL 并转换字符串类型,适用于需要加强编译期检查的字符串密集型 API。
主要功能
- 01描述结构化字符串模式
- 02用 `infer` 解析字符串
- 03递归转换字符串类型
- 04编译期验证格式
- 05与映射类型结合
适用场景
- 01为 ID、路径和 URL 添加类型安全
- 02解析 CSS 选择器或查询语言
- 03将 snake_case 转为 camelCase
- 04验证 API 中的字符串格式
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill template-literal-types安装后在 Claude Code 中输入/template-literal-types确认已加载。
详细介绍
使用模板字面量类型建模 DSL 和字符串关系
概述
模板字面量类型将 JavaScript 模板字面量的强大功能引入 TypeScript 类型系统。它们可以帮助你建模结构化的字符串子集、解析领域特定语言(DSL),并捕获字符串类型之间的关系。结合条件类型和 infer 关键字,它们能够在类型层面实现复杂的字符串操作。
这项技能对于为字符串密集型 API 提供类型安全,以及构建强大的类型转换至关重要。
何时使用这项技能
- 建模结构化字符串模式(ID、路径、URL)
- 解析领域特定语言(CSS 选择器、查询语言)
- 转换字符串类型(camelCase、snake_case 转换)
- 在编译时验证字符串格式
- 与映射类型结合进行键转换
铁律
使用模板字面量类型建模结构化字符串子集和 DSL。结合 infer 进行解析,结合映射类型进行转换。
检测
留意以下适用场景:
// RED FLAGS - Untyped strings that could be precise
type EventType = string; // Could be 'click' | 'hover' | etc.
function query(selector: string): Element; // Could parse CSS selectors
type CSSProperty = string; // Could validate property names
基本模板字面量类型
// Match strings starting with a prefix
type PseudoString = `pseudo${string}`;
const science: PseudoString = 'pseudoscience'; // OK
const alias: PseudoString = 'pseudonym'; // OK
const physics: PseudoString = 'physics'; // Error!
// Match specific patterns
type DataAttribute = `data-${string}`;
type HTTPSUrl = `https://${string}`;
type VersionString = `v${number}.${number}.${number}`;
使用模板字面量的索引签名
// Allow data-* attributes while keeping type safety
interface Checkbox {
id: string;
checked: boolean;
[key: `data-${string}`]: unknown;
}
const check: Checkbox = {
id: 'subscribe',
checked: true,
'data-listIds': 'all-the-lists', // OK
value: 'yes', // Error: not data-* and not known property
};
使用 infer 进行解析
使用带有 infer 的条件类型提取字符串中的部分内容:
// Extract event name from handler type
type EventName<T> = T extends `on${infer Name}` ? Name : never;
type ClickEvent = EventName<'onClick'>; // 'Click'
type HoverEvent = EventName<'onMouseEnter'>; // 'MouseEnter'
type BadEvent = EventName<'handleClick'>; // never
// Extract path parameters
type PathParams<T> = T extends `/users/${infer UserId}/posts/${infer PostId}`
? { userId: UserId; postId: PostId }
: never;
type Params = PathParams<'/users/123/posts/456'>;
// { userId: '123'; postId: '456' }
字符串转换
构建递归类型来转换字符串:
// Convert snake_case to camelCase
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S;
type T1 = CamelCase<'foo'>; // 'foo'
type T2 = CamelCase<'foo_bar'>; // 'fooBar'
type T3 = CamelCase<'foo_bar_baz'>; // 'fooBarBaz'
// Apply to object keys
type CamelCaseKeys<T> = {
[K in keyof T as CamelCase<K & string>]: T[K]
};
type SnakeCase = { user_name: string; email_address: string };
type Camel = CamelCaseKeys<SnakeCase>;
// { userName: string; emailAddress: string }
真实示例:CSS 选择器
// Enhance querySelector with precise types
type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
interface ParentNode {
// Simple tag selector
querySelector<TagName extends HTMLTag>(
selector: TagName
): HTMLElementTagNameMap[TagName] | null;
// Tag#id selector
querySelector<TagName extends HTMLTag>(
selector: `${TagName}#${string}`
): HTMLElementTagNameMap[TagName] | null;
}
}
// Usage
const img = document.querySelector('img#hero');
// Type: HTMLImageElement | null
// Can access img?.src, img?.alt, etc.
const div = document.querySelector('div#container');
// Type: HTMLDivElement | null
与映射类型结合
// Create event handler types from event names
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
submit: SubmitEvent;
};
type EventHandlers<Events extends Record<string, Event>> = {
[K in keyof Events as `on${Capitalize<K & string>}`]?:
(event: Events[K]) => void;
};
type Handlers = EventHandlers<EventMap>;
// {
// onClick?: (event: MouseEvent) => void;
// onKeydown?: (event: KeyboardEvent) => void;
// onSubmit?: (event: SubmitEvent) => void;
// }
抗压应对流程
当有人施压要求使用简单的 string 类型时:
- 识别模式:这些字符串有什么结构?
- 从简单开始:先使用字面量类型联合
- 添加模板:使用模板字面量表示无限但有结构的集合
- 考虑解析:使用
infer提取信息 - 测试边界情况:确保类型准确,而不只是精确
危险信号
| 反模式 | 问题所在 |
|---|---|
type ID = string | 错失验证机会 |
| 未经测试的复杂模板类型 | 可能不准确 |
| 解析时没有逃生口 | 复杂选择器需要回退方案 |
| 类型过于精确 | 可能破坏合理的使用场景 |
常见借口
“String 类型足够好了”
实际情况:模板字面量能在编译时捕获拼写错误和无效格式。例如,'user-123' 和 'users-123' 之间的区别可以立即发现。
“这太复杂了”
实际情况:从简单的前缀模式开始,再按需增加复杂度。即使是基本的模板字面量也很有价值。
“这会影响性能”
实际情况:模板字面量类型在编译时求值,不会产生运行时开销。
快速参考
| 模式 | 语法 | 使用场景 |
|---|---|---|
| 前缀 | `data-${string}` | data 属性 |
| 后缀 | `${string}Event` | 事件名称 |
| 中间 | `${string}.${string}` | 文件扩展名 |
| 提取 | `on${infer Name}` | 解析 |
| 转换 | `${Head}${Capitalize<Tail>}` | camelCase |
总结
模板字面量类型为字符串密集型代码带来类型安全。使用它们建模结构化字符串、解析 DSL 和转换类型。结合 infer 和映射类型,可以实现强大的类型级字符串操作。
参考资料
- Dan Vanderkam 著《Effective TypeScript》第 2 版
- 条目 54:使用模板字面量类型建模 DSL 和字符串之间的关系
评论
支持 Markdown