TypeScript 可扩展联合类型

问题背景

在日常开发过程中,我们经常会遇到一些类型定义,它们既有内置的可穷举的字面量值,又要支持开发者自己输入。比如 CSScolor 属性,既可以用 redgreen 这些内置的颜色值,也可以用 #ff0000 这样的十六进制值。

传统联合类型的局限性

这种情况下,我们可能会首先想到使用 TypeScript 中的联合类型 (Union Type) 来定义这个类型:

1
type Color = 'red' | 'green' | 'blue' | string

但是这样定义存在一个问题:最终 Color 的类型其实是 string,编写代码时编辑器不会给出任何特定的颜色提示。

可扩展联合类型的解决方案

为了解决这个问题,我们可以使用可扩展联合类型:

1
type Color = 'red' | 'green' | 'blue' | (string & {})

原理与作用

type Color = 'red' | 'green' | 'blue' | (string & {}) 这段代码创建了一个联合类型,它包含了两部分:

  1. 'red' | 'green' | 'blue':一个字面量类型,表示精确匹配字符串 'red''green''blue'
  2. (string & {}):这部分是核心,它创建了一个特殊的可扩展类型

为什么 (string & {}) 是可扩展的?

TypeScript 中,{} 代表一个空对象类型,它可以匹配任何非 nullundefined 的值。当一个类型与 {} 进行交叉(&)时,结果会是原类型,但它会告诉 TypeScript 编译器这个类型可以被扩展。

所以,string & {} 的结果就是 string 类型,但它带上了一个特殊的”标记”。

最终效果

最终,type Color 的完整类型是 string,但它包含了一个额外的”字面量提示”。这意味着当你使用 Color 类型时,IDE 会优先提示 'red' 这个值,但同时允许你传入任何其他字符串。

核心优势

'red' | 'green' | 'blue' | (string & {}) 这种语法是一种巧妙的组合,它的作用是:

  • 提供类型安全:它确保传入的值必须是字符串
  • 改善开发者体验:它通过字面量提示,让开发者知道哪些是常用的、推荐的值
  • 保持灵活性:它允许你传入任何其他字符串,而不仅仅局限于预设的几个值

应用场景

这种模式在构建组件库时尤其有用,因为它能在提供友好提示的同时,不限制用户的自定义能力。例如:

1
2
3
4
5
6
7
8
// UI 组件的尺寸定义
type Size = 'small' | 'medium' | 'large' | (string & {})

// 主题颜色定义
type ThemeColor = 'primary' | 'secondary' | 'danger' | (string & {})

// 动画类型定义
type Animation = 'fade' | 'slide' | 'bounce' | (string & {})

通过这种方式,我们既能为常用场景提供良好的开发体验,又能保持 API 的灵活性和扩展性。