首页 > 解决方案 > 在打字稿中获取枚举键作为联合字符串的通用类型?

问题描述

考虑以下打字稿枚举:

enum MyEnum { A, B, C };

如果我想要另一种类型,即该枚举键的联合字符串,我可以执行以下操作:

type MyEnumKeysAsStrings = keyof typeof MyEnum;  // "A" | "B" | "C"

这非常有用。

现在我想创建一个以这种方式在枚举上普遍运行的泛型类型,这样我就可以说:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<MyEnum>;

我想正确的语法是:

type AnyEnumKeysAsStrings<TEnum> = keyof typeof TEnum; // TS Error: 'TEnum' only refers to a type, but is being used as a value here.

但这会产生编译错误:“'TEnum' 仅指一种类型,但在此处用作值。”

这是出乎意料和悲伤的。通过从泛型声明的右侧删除 typeof 并将其添加到特定类型声明中的类型参数中,我可以通过以下方式完全解决它:

type AnyEnumAsUntypedKeys<TEnum> = keyof TEnum;
type MyEnumKeysAsStrings = AnyEnumAsUntypedKeys<typeof MyEnum>; // works, but not kind to consumer.  Ick.

不过我不喜欢这种解决方法,因为这意味着消费者必须记住在泛型上对 typeof 进行这种令人讨厌的指定。

是否有任何语法可以让我按照我最初想要的方式指定泛型类型,以便对消费者友好?

标签: typescriptgenericsenums

解决方案


不,消费者需要使用typeof MyEnum来引用键为AB和的对象C


前面有很长的解释,其中一些你可能已经知道了

您可能知道,TypeScript 为 JavaScript 添加了一个静态类型系统,并且在代码被转译时该类型系统会被删除。TypeScript 的语法是这样的,一些表达式和语句引用运行时存在的,而其他表达式和语句引用仅在设计/编译时存在的类型。具有类型,但它们本身不是类型。重要的是,在代码中的某些地方,编译器会期望一个值并将它找到的表达式解释为一个值,如果可能的话,编译器会期望一个类型并将它找到的表达式解释为一个类型。

如果一个表达式可能被解释为一个值和一个类型,编译器不会在意或感到困惑。null例如,对于以下代码中 的两种风格,它非常满意:

let maybeString: string | null = null;

第一个实例null是类型,第二个实例是值。它也没有问题

let Foo = {a: 0};
type Foo = {b: string};   

其中第一个Foo是命名值,第二个Foo是命名类型。注意值的类型Foo{a: number},而类型Foo{b: string}。他们不一样。

即使是typeof操作员也过着双重生活。 表达式typeof x总是期望x是一个值,但typeof x它本身可能是一个值或类型,具体取决于上下文:

let bar = {a: 0};
let TypeofBar = typeof bar; // the value "object"
type TypeofBar = typeof bar; // the type {a: number}

该行将let TypeofBar = typeof bar;通过 JavaScript,它将在运行时使用JavaScript typeof 运算符并生成一个字符串。但是type TypeofBar = typeof bar; 被删除,它使用TypeScript 类型查询运算符来检查 TypeScript 分配给名为 的值的静态类型bar


现在,TypeScript 中大多数引入名称的语言结构都创建了命名值或命名类型。下面是一些命名值的介绍:

const value1 = 1;
let value2 = 2;
var value3 = 3;
function value4() {}

以下是命名类型的一些介绍:

interface Type1 {}
type Type2 = string;

但是有一些声明同时创建命名值命名类型,并且像Foo上面一样,命名值的类型不是命名类型。大的是classenum

class Class { public prop = 0; }
enum Enum { A, B }

这里,type Class是 的实例的类型Class,而value Class构造函数对象。而且typeof Class不是Class

const instance = new Class();  // value instance has type (Class)
// type (Class) is essentially the same as {prop: number};

const ctor = Class; // value ctor has type (typeof Class)
// type (typeof Class) is essentially the same as new() => Class;

并且,类型 Enum是枚举元素的类型;每个元素的类型的联合。而value Enum是一个对象,其键是Aand B,其属性是枚举的元素。而且typeof Enum不是Enum

const element = Math.random() < 0.5 ? Enum.A : Enum.B; 
// value element has type (Enum)
// type (Enum) is essentially the same as Enum.A | Enum.B
//  which is a subtype of (0 | 1)

const enumObject = Enum;
// value enumObject has type (typeof Enum)
// type (typeof Enum) is essentially the same as {A: Enum.A; B: Enum.B}
//  which is a subtype of {A:0, B:1}

现在支持您的问题。你想发明一个像这样工作的类型运算符:

type KeysOfEnum = EnumKeysAsStrings<Enum>;  // "A" | "B"

在哪里输入类型 Enum,然后取出对象 Enum的键。但正如您在上面看到的,类型Enum与对象不同Enum。不幸的是,该类型对值一无所知。这有点像这样说:

type KeysOfEnum = EnumKeysAsString<0 | 1>; // "A" | "B"

显然,如果你这样写,你会发现你无法对0 | 1产生 type的类型做任何事情"A" | "B"。为了让它工作,你需要传递一个知道映射的类型。而那种类型是typeof Enum...

type KeysOfEnum = EnumKeysAsStrings<typeof Enum>; 

这就像

type KeysOfEnum = EnumKeysAsString<{A:0, B:1}>; // "A" | "B"

可能的......如果type EnumKeysAsString<T> = keyof T


所以你被困在让消费者指定typeof Enum。有解决方法吗?好吧,您也许可以使用具有该值的东西,例如函数?

 function enumKeysAsString<TEnum>(theEnum: TEnum): keyof TEnum {
   // eliminate numeric keys
   const keys = Object.keys(theEnum).filter(x => 
     (+x)+"" !== x) as (keyof TEnum)[];
   // return some random key
   return keys[Math.floor(Math.random()*keys.length)]; 
 }

然后你可以打电话

 const someKey = enumKeysAsString(Enum);

的类型someKey将是"A" | "B"。是的,但是要使用它作为类型,你必须查询它:

 type KeysOfEnum = typeof someKey;

这迫使您typeof再次使用并且比您的解决方案更冗长,特别是因为您不能这样做:

 type KeysOfEnum = typeof enumKeysAsString(Enum); // error

布莱。对不起。


回顾:

  • 这是不可能的;
  • 类型和价值 blah blah;
  • 仍然不可能;
  • 对不起。

希望这有点道理。祝你好运。


推荐阅读