mcpField
T
template-literal-types

template-literal-types

github.com/marius-townhouse开发646 次安装39 周前更新
代码仓库

关于 template-literal-types

这项 TypeScript 技能讲解如何用模板字面量类型描述结构化字符串、解析 DSL 并转换字符串类型,适用于需要加强编译期检查的字符串密集型 API。

主要功能

  1. 01描述结构化字符串模式
  2. 02用 `infer` 解析字符串
  3. 03递归转换字符串类型
  4. 04编译期验证格式
  5. 05与映射类型结合

适用场景

  1. 01为 ID、路径和 URL 添加类型安全
  2. 02解析 CSS 选择器或查询语言
  3. 03将 snake_case 转为 camelCase
  4. 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 类型时:

  1. 识别模式:这些字符串有什么结构?
  2. 从简单开始:先使用字面量类型联合
  3. 添加模板:使用模板字面量表示无限但有结构的集合
  4. 考虑解析:使用 infer 提取信息
  5. 测试边界情况:确保类型准确,而不只是精确

危险信号

反模式问题所在
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