泛型类型获取枚举键作为联合字符串在类型脚本?

考虑下面的输入脚本枚举:

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 参数中:

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

但是我不喜欢这种变通方法,因为它意味着使用者必须记住在泛型上指定 typeof。

是否有任何语法允许我指定我最初想要的泛型类型,以满足消费者的需求?

60635 次浏览

不,使用者将需要使用 typeof MyEnum来引用键为 ABC的对象。


接下来还有很长的解释,其中一些你可能已经知道了

您可能已经知道,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本身可以是值或类型,这取决于上下文:

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 类型的运算符并生成一个字符串。但是 type TypeofBar = typeof bar; 被擦除,并且它正在使用 类型查询运算符检查 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 }

在这里,类型 ClassClass例子的类型,而 价值 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是枚举的 元素的类型; 是每个元素类型的并集。而 价值Enum对象,其键是 AB,其属性是枚举的元素。而 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"

很明显,如果你这样写,你就会发现对于产生 "A" | "B"类型的 0 | 1类型你什么也做不了。为了使其正常工作,您需要向它传递一个知道映射的类型。这种类型是 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

对不起。


回顾:

  • 这是不可能的;
  • 类型和价值观之类的;
  • 仍然不可能;
  • 抱歉。

希望你能理解,祝你好运。

确实有可能。

enum MyEnum { A, B, C };


type ObjectWithValuesOfEnumAsKeys = { [key in MyEnum]: string };


const a: ObjectWithValuesOfEnumAsKeys = {
"0": "Hello",
"1": "world",
"2": "!",
};


const b: ObjectWithValuesOfEnumAsKeys = {
[MyEnum.A]: "Hello",
[MyEnum.B]: "world",
[MyEnum.C]: "!",
};


// Property '2' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithValuesOfEnumAsKeys = {  //  Invalid! - Error here!
[MyEnum.A]: "Hello",
[MyEnum.B]: "world",
};


// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithValuesOfEnumAsKeys = {
[MyEnum.A]: "Hello",
[MyEnum.B]: "world",
[MyEnum.C]: "!",
6: "!",  //  Invalid! - Error here!
};

游乐场连结


编辑: 解除限制!

enum MyEnum { A, B, C };


type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = { [key in enumValues]: string };


const a: ObjectWithKeysOfEnumAsKeys = {
A: "Hello",
B: "world",
C: "!",
};


// Property 'C' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithKeysOfEnumAsKeys = {  //  Invalid! - Error here!
A: "Hello",
B: "world",
};


// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithKeysOfEnumAsKeys = {
A: "Hello",
B: "world",
C: "!",
D: "!",  //  Invalid! - Error here!
};

游乐场连结


  • 这与 const enum也工作!

只需要传递一个 type而不是 value,编译器就不会发出抱怨。正如您所指出的,这是您使用 typeof实现的。
将不那么自动化:

type AnyEnumKeysAsStrings<TEnumType> = keyof TEnumType;

你可以把它用作:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<typeof MyEnum>;

有一种解决方案不需要创建新的泛型类型。

如果声明枚举

enum Season { Spring, Summer, Autumn, Winter };

要获得该类型,只需使用关键字 keyof typeof

let seasonKey: keyof typeof Season;

然后变量就会按预期工作

seasonKey = "Autumn"; // is fine
// seasonKey = "AA" <= won't compile

如果我正确理解 OP 问题和 Akxe 答案: 这里有一个可能的进一步简化。使用类型脚本实用程序。记录 < 键,键入 >

Https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type

例如:。

enum MyEnum { A, B, C };


type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = Record<enumValues, string>
const a: ObjectWithKeysOfEnumAsKeys = {
A: "PropertyA",
B: "PropertyB",
C: "PropertyC",
};