TypeScript 枚举的多种实现方式
TypeScript 中实现枚举的多种方式,包括传统枚举、const 对象、字面量联合类型、Unique Symbol 和品牌化类型,分析各种方式的优缺点和适用场景。
使用传统 TypeScript 枚举#
传统枚举是 TS 内置的语法糖,写法简洁,同时既可以作为类型又可以作为值使用。但缺点在于编译时会生成额外的代码,不适用于纯类型擦除的环境。
// 定义一个传统枚举,表示方向
enum Direction {
  Up,    // 默认值为 0
  Down,  // 默认值为 1
  Left,  // 默认值为 2
  Right  // 默认值为 3
}
/**
 * 根据传入的方向进行处理
 * @param direction - 枚举类型 Direction 的一个成员
 */
function move(direction: Direction) {
  // 根据枚举变量进行分支判断
  switch (direction) {
    case Direction.Up:
      console.log('Moving up')
      break
    case Direction.Down:
      console.log('Moving down')
      break
    case Direction.Left:
      console.log('Moving left')
      break
    case Direction.Right:
      console.log('Moving right')
      break
  }
}
// 使用时直接利用枚举对象访问成员
move(Direction.Left)
// 优点:语法简单、直观,TypeScript 能同时利用类型和值支持。
// 缺点:会生成额外的运行时代码,在仅做类型擦除的场景下可能不适用。typescript利用 const 对象与 as const 实现枚举替代方案#
这种方式借助对象字面量以及 as const 断言,保证生成的 JS 代码和手写代码一致,同时利用 TypeScript 提供的字面量类型推导生成一个联合类型。
// 定义一个包含各个方向的对象,并通过 as const 让每个属性都是字面量类型
const DirectionObj = {
  Up: 'up',
  Down: 'down',
  Left: 'left',
  Right: 'right'
} as const
// 利用 ValueOf 辅助类型(取对象值的联合类型),得到 "up" | "down" | "left" | "right" 的联合类型
type DirectionObj = (typeof DirectionObj)[keyof typeof DirectionObj]
/**
 * 根据传入的方向(联合类型)进行处理
 * @param direction - 只能是 "up"、"down"、"left" 或 "right"
 */
function move(direction: DirectionObj) {
  if (direction === DirectionObj.Up) {
    console.log('Moving up')
  } else if (direction === DirectionObj.Down) {
    console.log('Moving down')
  } else if (direction === DirectionObj.Left) {
    console.log('Moving left')
  } else if (direction === DirectionObj.Right) {
    console.log('Moving right')
  } else {
    // 理论上永远不会到这里,因为 direction 类型被严格限定
    console.error('Invalid direction')
  }
}
// 直接使用对象的成员调用函数
move(DirectionObj.Right)
// 优点:生成的代码与书写的对象字面量一致,没有额外代码,适用于代码体积敏感的环境;同时可组合、迭代、与 JS 操作无缝结合。
// 缺点:声明稍长,需要通过辅助类型 ValueOf 来提取联合类型,对比枚举语法来说稍显冗长。typescript让我们分步解释:
- 
const DirectionObj = { ... } as const- 
const DirectionObj = { ... }: 这声明了一个名为DirectionObj 的常量(const)变量,并将其初始化为一个对象字面量({ ... })。这里的{ ... } 代表该对象包含一些键值对,例如:
typescriptconst DirectionObj = { Up: 'up', Down: 'down', Left: 'left', Right: 'right' } - 
as const: 这是 TypeScript 中的 const 断言 (const assertion) 。它告诉 TypeScript:- 
推断出最具体的类型,而不是通用的类型。例如,
'up' 的类型被推断为字面量类型'up',而不是string。 - 
将所有属性标记为
readonly。 - 
对于数组,将其视为
readonly 元组。 - 
在这个例子中,应用
as const 后,DirectionObj 常量的类型会被推断为:
typescript// (假设使用了上面的例子) // typeof DirectionObj 的类型是: Readonly<{ Up: 'up' Down: 'down' Left: 'left' Right: 'right' }> 
 - 
 
 - 
 - 
type DirectionObj = (typeof Status)[keyof typeof DirectionObj] - 
type DirectionObj = ...: 这声明了一个名为DirectionObj 的类型别名 (Type Alias) 。注意,这里的DirectionObj 是一个 类型,它与上面定义的DirectionObj 常量 不同(TypeScript 允许值和类型共享名称,因为它们存在于不同的命名空间)。 - 
typeof DirectionObj:typeof 操作符在这里用于获取DirectionObj 常量 的类型。正如上面推断的,它的类型是Readonly<{ Up: "up"; Down: "down"; Left: "left"; Right: "right"; }>。 - 
keyof typeof DirectionObj:keyof 操作符用于获取一个类型的所有公共属性名的联合类型。对于typeof DirectionObj,keyof 会得到'Up' | 'Down' | 'Left' | 'Right'。 - 
(typeof DirectionObj)[keyof typeof DirectionObj]: 这是索引访问类型 (Indexed Access Types) 或 查找类型 (Lookup Types) 。它会查找typeof DirectionObj 类型中,由keyof typeof DirectionObj(即'Up' | 'Down' | 'Left' | 'Right')指定的那些键所对应的 值 的类型。- 
DirectionObj['Up'] 的类型是'up' - 
DirectionObj['Down'] 的类型是'down' - 
DirectionObj['Left'] 的类型是'left' - 
DirectionObj['Right'] 的类型是'right' - 因此,
(typeof DirectionObj)[keyof typeof DirectionObj] 的结果是这些值类型的联合类型:'up' | 'down' | 'left' | 'right'。 
 - 
 - 
对于这行代码,还有另一种写法
typescript// 辅助类型:将对象 T 的所有属性值联合起来 type ValueOf<T> = T[keyof T] type DirectionObj = ValueOf<typeof DirectionObj>这两种写法在类型层面上是完全等效的。很多时候,我们会定义一个辅助类型别名(常叫做
ValueOf<T>)来提取一个对象所有属性值构成的联合类型。它取得了DirectionObj 的所有键(keyof typeof DirectionObj)对应的值,并生成一个联合类型。因此,本质上它们没有区别,只是一个是把操作封装成了一个通用的别名,以便在多个地方重用,而这种写法在这个例子中则是内联展开,避免了额外的类型别名声明。这种选择主要是代码风格和复用性上的考虑。如果项目中有多个场景需要提取对象值的类型,使用
ValueOf 辅助类型会让代码更简洁和具有一致性;而在示例中直接内联写出相同表达式也能达到相同的效果。 
 - 
 
总结:
这段代码的目的是:
- 定义一个包含一组相关常量值(通常是字符串或数字)的对象 
DirectionObj。as const 确保这些值被推断为精确的字面量类型并且对象是只读的。 - 基于这个常量对象的 值,自动创建一个联合类型 
DirectionObj。这个联合类型包含了对象中所有值的字面量类型。 
使用字面量联合类型#
如果仅需要一个离散的值集合,而不关心既能作为类型也能作为值的语法糖,直接使用联合类型也是一种简单方案。
// 直接定义一个字符串联合类型,表示方向
type Direction = 'up' | 'down' | 'left' | 'right'
/**
 * 根据传入的方向进行响应处理,switch 结构实现分支判断
 * @param direction - 必须是 "up"、"down"、"left" 或 "right"
 */
function move(direction: Direction) {
  switch (direction) {
    case 'up':
      console.log('Moving up')
      break
    case 'down':
      console.log('Moving down')
      break
    case 'left':
      console.log('Moving left')
      break
    case 'right':
      console.log('Moving right')
      break
    default:
      // 一般不会进入,TypeScript 会通过穷尽检查确保覆盖所有情况
      console.error('Invalid direction')
  }
}
move('up')
// 优点:不需要额外的对象包装,书写简单。
// 缺点:仅作为类型,若希望同时具备作为值的便利(例如重复引用、迭代)则不如枚举或 const 对象灵活。ts使用 Unique Symbol 实现名义类型枚举#
在 TypeScript 中,默认采用的是基于结构的类型系统。这意味着只要对象的结构一致,类型就可以相互兼容。但有时我们希望某个类型具有名义(nominal)的特性,也就是“看名字”来判断类型而不是“看结构”。而 unique symbol 就是一种实现这一目标的工具。
// 利用 unique symbol 定义每个方向的唯一标识
const UP: unique symbol = Symbol('Up')
const DOWN: unique symbol = Symbol('Down')
const LEFT: unique symbol = Symbol('Left')
const RIGHT: unique symbol = Symbol('Right')
// 定义一个 const 对象,将每个成员映射到对应的 unique symbol
const DirectionSymbol = {
  Up: UP,
  Down: DOWN,
  Left: LEFT,
  Right: RIGHT
} as const
// 利用 TypeScript 的类型推导,构造一个联合类型,代表所有的枚举值
type Direction = (typeof DirectionSymbol)[keyof typeof DirectionSymbol]
/**
 * 根据传入的方向(symbol 类型)进行处理
 * @param direction - 只能是 DirectionSymbol 中的某个唯一 symbol
 */
function move(direction: Direction) {
  if (direction === DirectionSymbol.Up) {
    console.log('Moving up')
  } else if (direction === DirectionSymbol.Down) {
    console.log('Moving down')
  } else if (direction === DirectionSymbol.Left) {
    console.log('Moving left')
  } else if (direction === DirectionSymbol.Right) {
    console.log('Moving right')
  }
}
move(DirectionSymbol.Down)ts- 
Unique Symbol 的含义 当你声明一个变量并标记为
unique symbol,TypeScript 会将该变量的类型视为一个独一无二的字面量类型,而不仅仅是普通的symbol 类型。这就意味着即使两个变量都是unique symbol 类型,它们各自的类型也是彼此不兼容的,除非显式地传递了同一个变量引用。 - 
如何使用 在代码中,我们为每个枚举成员创建了一个
const 声明,并使用unique symbol 来标记它们:
typescript// 为每个方向声明一个 unique symbol(每个符号都具有独特的类型) const UP: unique symbol = Symbol('Up') const DOWN: unique symbol = Symbol('Down') const LEFT: unique symbol = Symbol('Left') const RIGHT: unique symbol = Symbol('Right')接着,我们将这些符号放入一个对象中,并使用
as const 固定其属性,确保对象属性的类型不会丢失:
typescript// 组合成一个不可变的对象,确保各个属性的类型都是唯一的 symbol 字面量 const DirectionSymbol = { Up: UP, Down: DOWN, Left: LEFT, Right: RIGHT } as const最后,通过下面这种写法:
typescripttype Direction = (typeof DirectionSymbol)[keyof typeof DirectionSymbol]我们就获取了
DirectionSymbol 对象中所有属性值组成的联合类型。这意味着Direction 类型只能为DirectionSymbol 中某个具体的成员(即某个 unique symbol)。 - 
优缺点
- 
优点:
- 严格的类型保护。由于每个 unique symbol 的类型都是独唯一的,这种方式可以防止不同枚举之间混用,达到名义类型检查的效果。
 - 类型安全。在开发过程中,TypeScript 能够准确识别每个符号,避免一些由于结构相似而出现的类型错误。
 
 - 
缺点:
- 调试和序列化问题。运行时产生的值为 symbol,调试时输出的值不直观,而且 symbol 不能直接 JSON 序列化。
 - 不可枚举。如果希望在运行时代码中枚举所有成员,就需要额外的工作,因为 symbol 属性不像普通的字符串那样容易遍历。
 
 
 - 
 
利用品牌 (Branded) 技巧实现更严格的枚举类型#
在 TypeScript 中,由于采用的是结构化类型系统,即使两个类型内部结构一致也可以互相赋值。有时候这会带来隐患,比如不同业务逻辑下虽然使用的是字符串,但你不希望“green”这个字符串既代表颜色又代表其他意思。所谓“品牌化”(Branding)技术,就是通过在类型上额外加上一个虚拟标记,从而使得相同的基本类型在类型系统中被视为完全不同的类型。
// 定义辅助类型 ValueOf,用来提取对象中所有属性的联合类型
type ValueOf<T> = T[keyof T]
// 定义品牌包装类型,用于在原始类型上添加一个独特的标记字段
type Brand<T, B> = T & { __brand: B }
// 定义一个颜色映射对象,并用 as const 固定属性类型
const ColorMapping = {
  Red: 'red',
  Green: 'green',
  Blue: 'blue'
} as const
// 定义一个颜色类型,利用 Brand 技巧将普通的字符串类型包装为独特的 Color 类型
type Color = Brand<ValueOf<typeof ColorMapping>, 'Color'>
/**
 * 一个函数,要求传入已带品牌的 Color 类型
 * @param color - 传入的颜色必须是经过品牌包装后的类型
 */
function paint(color: Color) {
  console.log(`Painting in ${color}`)
}
/**
 * 辅助函数,将对象的键转换为带品牌的 Color 类型
 * 这样可以确保只有预定义的枚举值才能得到正确的类型
 * @param key - ColorMapping 对象的键("Red" | "Green" | "Blue")
 * @returns 对应带品牌的 Color 类型值
 */
const makeColor = (key: keyof typeof ColorMapping): Color => {
  // 强制断言返回值为 Color (带有品牌__brand)
  return ColorMapping[key] as Color
}
const myColor = makeColor('Green')
paint(myColor)
// 如果直接使用普通字面量会报错,例如下面这行(取消注释会产生类型错误):
// paint("green");ts- 
品牌包装 我们定义一个辅助类型
Brand<T, B>:
typescripttype Brand<T, B> = T & { __brand: B }其作用是将原始类型
T(例如string)与一个标记属性__brand 结合,从而得到一种新的类型。这个__brand 字段只在类型层面存在,用于区分两个结构上完全相同的值。 - 
如何应用 以颜色枚举为例,我们有一个对象定义了各个颜色:
typescriptconst ColorMapping = { Red: 'red', Green: 'green', Blue: 'blue' } as const接着,利用
Brand 辅助类型和上面提到的提取所有属性值组成联合类型的技巧,我们定义:
typescript// 这里 ValueOf<T> 表示取对象中所有属性值的联合类型 type ValueOf<T> = T[keyof T] // 定义 Color 类型,外面加上品牌 "Color" type Color = Brand<ValueOf<typeof ColorMapping>, 'Color'>这样,即使实际内容上 Color 就是字符串类型,但它携带了一个独特的品牌。
再使用一个辅助函数将合法的颜色转换成带品牌的类型:
typescriptconst makeColor = (key: keyof typeof ColorMapping): Color => { return ColorMapping[key] as Color }这样只有通过这个函数生成的值才能被视为符合
Color 类型,而直接写"green" 则不满足品牌要求,从而避免类型混用。 - 
优缺点
- 
优点:
- 利用品牌包装,能在编译阶段防止因结构一致而导致的错误赋值,增强了类型安全性。
 - 明确区分了不同业务逻辑下的相同基本类型(如字符串)的用途。
 
 - 
缺点:
- 写法上比直接使用字符串或联合类型更繁琐,需要辅助函数和显式的类型断言。
 - 增加了代码的复杂度,使用者需要理解“品牌化”这一概念,可能对维护成本有所影响。
 
 
 - 
 
利用类型保护实现联合类型的窄化#
在处理联合类型时,我们通常希望在经过某些判断后能够“窄化”(narrow)出具体的子类型,以便安全地访问对应的属性。TypeScript 内置了一些简单的窄化方式(比如 typeof 操作符或通过判别联合的 tag 字段),但有时我们需要自定义更复杂的类型保护函数。对联合类型基于标签字段(discriminated union)的检查方法。通过自定义类型保护函数,可以在运行时对值的具体形态进行判断,实现精确的类型推导。
// 定义两个接口 A 和 B,利用 type.name 字段区分
interface A {
  type: { name: 'A' } // 标签为 "A"
  a: number
  // 此处 b 不存在(或可以声明为 undefined)
  b?: undefined
}
interface B {
  type: { name: 'B' } // 标签为 "B"
  b: number
  a?: undefined
}
// 定义一个联合类型,可能是 A 或 B
const aOrB: A | B = { type: { name: 'A' }, a: 42 }
/**
 * 类型保护函数,用于判断传入对象的 type.name 是否为预期的值
 * @param obj - 任意有 type 属性的对象
 * @param name - 希望匹配的名称(例如 "A" 或 "B")
 * @returns 如果匹配则返回 true,此时 TS 可将 obj 窄化为具有特定 type 的子类型
 */
function hasTypeName<Name extends string>(
  obj: { type: { name: string } },
  name: Name
): obj is { type: { name: Name } } {
  return obj.type.name === name
}
// 使用类型保护函数进行类型窄化
if (hasTypeName(aOrB, 'A')) {
  // 在此代码块中,aOrB 已经被 TypeScript 窄化为类型 A
  console.log("It is A, property 'a' value:", aOrB.a)
} else {
  // 这里 aOrB 被窄化为类型 B
  console.log("It is B, property 'b' value:", aOrB.b)
}
// 优点:通过自定义类型保护,可以在运行时判断并窄化联合类型,方便对不同情况作出处理。
// 缺点:如果标签失效或设计不当,可能导致错误的窄化;同时在复杂场景下需要额外的辅助函数来进行判断。ts- 
判别联合和类型保护 当你定义多个类型且它们共享一个公共字段(例如
type.name),可以利用这个字段来判断具体是哪一种情况。这就是“判别联合”(Discriminated Unions)的思想。但有时内置的判断不足以让编译器自动完成窄化,这时就需要自定义一个类型保护函数。 - 
自定义类型保护函数的写法 自定义类型保护函数的返回值使用
obj is SpecificType 的形式,告诉编译器:如果函数返回true,那么传入的对象就可以被视为SpecificType 类型。例如:
typescript// 定义两个接口,通过 type.name 字段区分 interface A { type: { name: 'A' } a: number } interface B { type: { name: 'B' } b: number } // 自定义类型保护函数:判断对象的 type.name 是否为预期值 function hasTypeName<Name extends string>( obj: { type: { name: string } }, name: Name ): obj is { type: { name: Name } } { return obj.type.name === name }上面这个函数的返回类型
obj is { type: { name: Name } } 告诉 TypeScript,在判断返回true 后,obj 的type.name 一定是传入的name。这样,在 if 代码块内,就能安全地窄化出具体类型。 - 
如何使用 假设我们有一个联合类型的变量:
typescriptconst aOrB: A | B = { type: { name: 'A' }, a: 42 }当我们使用类型保护函数进行判断时:
typescriptif (hasTypeName(aOrB, 'A')) { // 这里 aOrB 被窄化为 A,可以安全访问属性 a console.log("It is A, property 'a' value:", aOrB.a) } else { // 否则,aOrB 被窄化为 B,可以安全访问属性 b console.log("It is B, property 'b' value:", aOrB.b) }编译器通过自定义的类型保护函数,准确推断出在不同分支中
aOrB 的具体类型,从而保证访问属性时不会出错。 - 
优缺点
- 
优点:
- 自定义类型保护函数能扩展自动窄化机制,使得对联合类型的判断更灵活精确。
 - 可以在复杂条件下,通过多层判断实现精细化类型分支,提升代码的安全性。
 
 - 
缺点:
- 如果设计不当(例如标签字段不唯一或不一致),可能导致类型保护失败或产生误判。
 - 需要额外定义辅助函数,增加了代码量和维护难度。
 
 
 -