泛型类型获取枚举键作为typescript中的联合字符串?

at0kjp5o  于 2022-12-30  发布在  TypeScript
关注(0)|答案(5)|浏览(234)

考虑下面的打印脚本枚举:

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。
是否有任何语法允许我按照最初的要求指定泛型类型,以善待使用者?

ffx8fchx

ffx8fchx1#

不,使用者需要使用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总是期望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 operator并生成一个字符串。已擦除,并且它正在使用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 * ClassClass的 * instance * 的类型,而 * value * Class是 * constructor * 对象,并且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是一个 * object *,其键是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"

在这里你放入 * type * Enum,然后取出 * object * Enum的键,但是正如你在上面看到的,**类型Enum和对象Enum**是不一样的,不幸的是类型不知道任何关于值的信息,这有点像说:

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

很明显,如果你这样写,你会发现你不能对类型0 | 1做任何事情,这会产生类型"A" | "B",为了使它工作,你需要传递一个知道Map的类型,这个类型是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 *,你必须查询它:

type KeysOfEnum = typeof someKey;

这将迫使您再次使用typeof,甚至比您的解决方案更冗长,尤其是您无法执行以下操作:

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

呃。抱歉。
总结一下:

  • 这不可能
  • 类型和值等等
  • 仍然不可能
  • 对不起
hgncfbus

hgncfbus2#

其实是有可能的。

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!
};

Playground链接
编辑:取消限制!

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!
};

Playground链接

  • 这也适用于const enum!
weylhg0b

weylhg0b3#

有一种解决方案不需要创建新的泛型类型。
如果声明枚举

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

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

let seasonKey: keyof typeof Season;

然后变量按预期工作

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

pes8fvy94#

你可以只传递一个type而不是value,编译器不会抱怨,正如你所指出的,这是用typeof实现的。
只是不那么自然:

type AnyEnumKeysAsStrings<TEnumType> = keyof TEnumType;

您可以将其用作:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<typeof MyEnum>;
sdnqo3pr

sdnqo3pr5#

如果我正确理解了OP问题,Akxe回答:下面是一个可能的进一步简化。使用typescript类型实用程序。记录〈Keys,Type〉
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",
};

相关问题