将枚举用作字典键

48

我正在尝试为给定的枚举创建一个保证查找。也就是说,对于枚举的每个键,查找中应该恰好有一个值。我希望通过类型系统来保证这一点,这样如果枚举扩展了,我就不会忘记更新查找。我尝试了以下方法:

type EnumDictionary<T, U> = {
    [K in keyof T]: U;
};

enum Direction {
    Up,
    Down,
}

const lookup: EnumDictionary<Direction, number> = {
    [Direction.Up]: 1,
    [Direction.Down]: -1,
};

但是我遇到了一个奇怪的错误:

类型 '{ [Direction.Up]: number; [Direction.Down]: number; }' 不能赋值给类型 'Direction'。

我觉得这很奇怪,因为它说 lookup 的类型应该是 Direction 而不是 EnumDictionary<Direction, number>。我可以通过将 lookup 的声明更改为以下内容来确认此问题:

const lookup: EnumDictionary<Direction, number> = Direction.Up;

没有报错。

我如何为枚举创建一个查找类型,以确保枚举的每个值都将导致另一种不同类型的值?

TypeScript 版本:3.2.1


4个回答

53
你可以按照以下步骤操作:
type EnumDictionary<T extends string | symbol | number, U> = {
    [K in T]: U;
};

enum Direction {
    Up,
    Down,
}

const a: EnumDictionary<Direction, number> = {
    [Direction.Up]: 1,
    [Direction.Down]: -1
};

我发现这很令人惊讶,直到我意识到枚举可以被视为专门的联合类型
另一个变化是,枚举类型本身实际上成为每个枚举成员的联合。虽然我们还没有讨论联合类型,但你需要知道的是,对于联合枚举,类型系统能够利用它知道的枚举本身存在的确切值集合。
这样定义的EnumDictionary基本上就是内置的Record类型:
type Record<K extends string, T> = {
    [P in K]: T;
}

1
不错!我建议如果可能的话使用字符串值,否则当新枚举成员被添加时,你会得到奇怪的错误信息,比如“属性2在...中丢失”。 - Aleksey L.
1
注意:您必须使用所有枚举值,这使它不像一个字典。;) - Torben Koch Pløen
4
@TorbenRahbekKoch 我这样声明:类型 Dictionary<TKey extends string | symbol | number, TValue> = Partial<Record<TKey, TValue>>; - Shalom Peles
1
有没有办法让字典要求所有的键都来自枚举,而不需要字典包含枚举值的所有条目?每个枚举值都会出现错误“缺少以下内容...”。 - Fortytwo
1
为了回答自己的问题,这里是一个链接,是我在最终决定值得询问后不久发现的答案。https://dev59.com/QVQJ5IYBdhLWcg3w-K-I#52700831 - Fortytwo
显示剩余2条评论

51

截至 TypeScript 2.9,您可以使用语法 { [P in K]: XXX }

对于以下枚举:

enum Direction {
    Up,
    Down,
}
如果您希望所有枚举值都是必需的,请执行以下操作:
const directionDictAll: { [key in Direction] : number } = {
    [Direction.Up]: 1,
    [Direction.Down]: -1,
}

或者,如果您只想在枚举中有值,但是可以有任意数量的值,您可以像这样添加?

const directionDictPartial: { [key in Direction]? : number } = {
    [Direction.Up]: 1,
}

2
我不知道这个“?”的技巧,谢谢分享! - K Mehta
在 TypeScript 4.5.2 中出现错误 类型“Direction”不能赋值给类型“string” - Ross

3

使用更新的TypeScript,您可以继续前进

Partial<Record<keyof typeof Direction, number>>

如果您想要所有键都是必需的,请选择

Record<keyof typeof Direction, number>

2
鉴于您的使用情况,我将分享一种常用的策略来解决这种问题,虽然它并不是严格的枚举方法。
首先,我创建了一个只读的as const数据结构——通常一个数组就足够了...
const SIMPLES = [
    "Love",
    "Hate",
    "Indifference",
    "JellyBabies"
  ] as const;

然后我可以简单地使用映射类型来获取数字条目...

  type SimpleCase = (typeof SIMPLES)[number];

enter image description here

我喜欢这种方法的两个方面...

  • 运行时访问
  • 可扩展性。

运行时访问

通常,您可以编写程序来确保您没有遗漏任何内容,而不仅仅是将问题视为红线或编译时错误...

const caseLabels = CASES.map((item) => item.toUpperCase());

可扩展性

你可以轻松遍历任何使用as const定义的数据结构,这意味着你不必不必要的处理类型系统来定义例如对于类型的映射。但是,当你需要时,你可以选择使用as const方案,因为它使得Typescript编译器可以'检查'你选择的类型,从而可以使用Mapped Types。

const COMPLEXES = {
  "Love":{letters:4},
  "Hate":"corrodes",
  "Indifference":() => console.log,
  "JellyBabies": [3,4,5]
} as const;

type Complex = keyof typeof COMPLEXES;
type ComplexRecord = typeof COMPLEXES[keyof typeof COMPLEXES];

当然,一旦您的静态记录条目可以作为类型进行寻址,您就可以从它们组合其他数据结构,这些数据结构本身将针对原始数据结构强制实施穷尽性。请查看以下链接以查看提取为类型的密钥和值的示例图像:show keys extracted as typesshow values extracted as types
  type ComplexProjection = {
    [K in Complex]:boolean;
  }

演示了在类型投影中的详尽性

因此,我从未使用过枚举,因为我认为语言本身已经足够强大,不需要它们。

请参考此Typescript Playground,以尝试上述类型所示的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接