简体   繁体   English

从数字中获取 TypeScript 枚举类型的密钥

[英]Getting keyof typeof TypeScript Enum from number

I have an enum:我有一个枚举:

enum ReviewReportType {
  HARASSMENT = 6,
  INAPPROPRIATE = 7,
  UNKNOWN_PERSON = 3,
  FAKE_REVIEW = 8,
  OTHER = 5,
}

and a Type:和一个类型:

export interface FirestoreReport {
  reviewId: string;
  type: keyof typeof ReviewReportType;
  message: string;
}

And I've got a payload coming from an API I want to transform to the FirestoreReport type:我有一个来自 API 的有效负载,我想转换为FirestoreReport类型:

const payload = {
  type: 6,
  message: "foo",
  reviewId: "bar"
}

I'm curious how to idiomatically map the type: 6 (which I can only ascertain to be of type number if I run the payload through zod ) into the keyof typeof ReviewReportType so that I end up with:我很好奇如何惯用 map type: 6 (如果我通过zod运行有效负载,我只能确定它的类型number )到keyof typeof ReviewReportType中,这样我最终得到:

const report: FirestoreReport = {
  type: "HARASSMENT", 
  message: "foo",
  reviewId: "bar"
}

My failed attempt:我失败的尝试:

const mapType = (type: number): keyof typeof ReviewReportType => {
  return ReviewReportType[type]
}

This gives me following error:这给了我以下错误:

Type 'string' is not assignable to type "'HARASSMENT" | "INAPPROPRIATE"...'

Currently, the reverse mapping for numeric enums is not strongly typed and is represented as a numeric index signature whose value type is string .目前, 数字枚举的反向映射不是强类型的,而是表示为值类型为string的数字索引签名 So if you index into a numeric enum with a number key, you will get a string output, as you noticed.因此,如果您使用number键对数字枚举进行索引,您将得到一个string output,正如您所注意到的。 This is too widely typed in the case where you pass in a valid enum member:在您传入有效的枚举成员的情况下,这类型过于广泛:

const str: "HARASSMENT" = ReviewReportType[ReviewReportType.HARASSMENT];
//    ^^^ <-- error, Type 'string' is not assignable to type '"HARASSMENT"'

This is the subject of microsoft/TypeScript#38806 , a feature request currently waiting for more community feedback.这是microsoft/TypeScript#38806的主题,目前正在等待更多社区反馈的功能请求。

And it's also too narrowly typed in the case where you pass in an invalid member, since it doesn't anticipate a possible undefined (unless you turn on the --noUncheckedIndexedAccess compiler option which most people don't do and isn't part of the --strict suite of compiler options ):而且在您传入无效成员的情况下,它的类型也太窄了,因为它没有预料到可能的undefined (除非您打开大多数人不这样做且不属于--noUncheckedIndexedAccess编译器选项--strict编译器选项套件):

const oops = ReviewReportType[123];
// const oops: string (but should really be string | undefined)
oops.toUpperCase(); // no compiler error but RUNTIME ERROR!

If you want to write a mapType() function that takes account of both of these in the "right" way, you can... but because the compiler doesn't handle it for you you'll need to use a type assertion to tell the compiler that ReviewReportType[type] is actually of the type you claim to return.如果您想编写一个mapType() function 以“正确”的方式考虑这两者,您可以...但是因为编译器不会为您处理它,您需要使用类型断言来告诉编译器ReviewReportType[type]实际上是您声称返回的类型。

Note that we could just use your version with a type assertion, like this:请注意,我们可以将您的版本与类型断言一起使用,如下所示:

const mapType = (num: number) => ReviewReportType[num] as keyof typeof ReviewReportType;

but it has very similar limitations... you get keyof typeof ReviewReportType instead of string , but it's still too wide但它有非常相似的限制......你得到keyof typeof ReviewReportType而不是string ,但它仍然太宽

const str: "HARASSMENT" = mapType(ReviewReportType.HARASSMENT); // still error

and too narrow而且太窄

const oops = mapType(123);
// const oops: "HARASSMENT" | "INAPPROPRIATE" | "UNKNOWN_PERSON" | "FAKE_REVIEW" | "OTHER"
oops.toUpperCase(); // still no compiler error but RUNTIME ERROR!

so you would need to be careful with it.所以你需要小心它。


Instead I'll write a generic version of mapType() that is as close to accurate as I can make it:相反,我将编写一个尽可能接近准确的mapType()通用版本:

const mapType = <N extends number>(num: N) => ReviewReportType[num] as
  { [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N ? P : never }[
  keyof typeof ReviewReportType] | (`${N}` extends `${ReviewReportType}` ? never : undefined)

That's quite a mouthful, but I'll try to explain it.这很拗口,但我会试着解释一下。 The function is generic in N the number - constrained type of num . function 在Nnumber 约束类型中是通用的num The return type is in two parts:返回类型分为两部分:

  • The { [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N? P: never }[keyof typeof ReviewReportType] { [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N? P: never }[keyof typeof ReviewReportType] { [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N? P: never }[keyof typeof ReviewReportType] is a distributive object type (as coined in ms/TS#47109 ), where we immediately index into a mapped type in order to distribute a type operation over the union of keys in the ReviewReportType enum. { [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N? P: never }[keyof typeof ReviewReportType]是一个分布式 object 类型(如ms/TS#47109中所创造),我们立即索引到映射类型,以便在ReviewReportType枚举中的键的联合上分配类型操作。 That operation is to check if the corresponding enum member is assignable to N .该操作是检查相应的枚举成员是否可分配给N If so we return the key, otherwise we return never .如果是,我们返回密钥,否则我们返回never So if N is 6 , then this will be "HARASSMENT" when the key is "HARASSMENT" , and never otherwise... the union of all of those is just "HARASSMENT" , which is what we want.所以如果N6 ,那么当键是 " "HARASSMENT"时这将是"HARASSMENT" ,否则never ......所有这些的联合只是"HARASSMENT" ,这就是我们想要的。 If N is wider, like number , you get all the keys (since each enum member extends number`).如果N更宽,比如number ,你会得到所有的键(因为每个枚举成员都扩展了 number`)。

  • The (`${N}` extends `${ReviewReportType}`? never: undefined) part checks to see if N can fail to be an enum member (I need to use template literal types to do this because numeric enums are considered narrower than the equivalent numeric literal types ; converting both sides to a string literal circumvents this). (`${N}` extends `${ReviewReportType}`? never: undefined)部分检查N是否不能成为枚举成员(我需要使用模板文字类型来执行此操作,因为数字枚举被认为更窄比等效的数字文字类型;将两边都转换为string文字绕过了这一点)。 If it can, then we want to add an undefined to the output type... otherwise we don't.如果可以,那么我们想在 output 类型中添加一个undefined ......否则我们不这样做。

Put those two together and you get the closest I can get to accurate behavior:把这两者放在一起,你就会得到最接近准确行为的结果:

  const str: "HARASSMENT" = mapType(ReviewReportType.HARASSMENT); // okay

That works now because mapType(ReviewReportType.HARASSMENT) returns "HARASSMENT" .现在可行,因为mapType(ReviewReportType.HARASSMENT)返回"HARASSMENT"

  const oops = mapType(123);
  // const oops: undefined
  oops.toUpperCase(); // compiler error now, oops is undefined

That is now a compiler error because mapType(123) returns undefined .现在这是一个编译器错误,因为mapType(123)返回undefined


Now we can use this as desired:现在我们可以根据需要使用它:

const report: FirestoreReport = {
  type: mapType(ReviewReportType.HARASSMENT), // okay
  message: "foo",
  reviewId: "bar"
}

This succeeds because the compiler knows ReviewReportType.HARASSMENT is 6 and that mapType(6) is "HARASSMENT" .这成功了,因为编译器知道ReviewReportType.HARASSMENT6并且mapType(6)"HARASSMENT" You mentioned that you're passing stuff through zod (whatever that is ) so the compiler will not know this.你提到你正在通过zod传递东西(不管那是什么),所以编译器不会知道这一点。 The compiler just knows it's some number :编译器只知道它是一些number

function getSomeNumber(): number {
  return Math.floor(Math.random() * 100);
}

And so you'll get an error:所以你会得到一个错误:

const report2: FirestoreReport = {
  type: mapType(getSomeNumber()), // error! could be undefined
  message: "",
  reviewId: ""
}

I contend that's the right behavior.我认为这是正确的行为。 The compiler should warn you if it can't verify that you're not assigning undefined there.如果编译器无法验证您没有在那里分配undefined ,它应该警告您。 You can fix that by using the non-null assertion operator ( ! ) :您可以通过使用非空断言运算符 ( ! )来解决此问题:

const report3: FirestoreReport = {
  type: mapType(getSomeNumber())!, // you *assert* that it's not
  message: "",
  reviewId: ""
}

But... maybe you hate that?但是……也许你讨厌那个?


If so then here's my final proposal.如果是这样,那么这是我的最终建议。 Make the function throw if the return value is going to be undefined , and remove the undefined possibility from the return type:如果返回值将是undefined则使 function throw ,并从返回类型中删除undefined的可能性:

const mapType = <N extends number>(num: N) => {
  const ret = ReviewReportType[num];
  if (typeof ret === "undefined") throw new Error("YOU GAVE ME " + num + " NOOOOOOOO!!!!! 😱");
  return ret as {
    [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N ? P : never
  }[keyof typeof ReviewReportType];
};

Now you know that any code running after a call to mapType() will have some key of the enum:现在您知道在调用mapType()之后运行的任何代码都将具有一些枚举键:

const str: "HARASSMENT" = mapType(ReviewReportType.HARASSMENT); // okay
const oops = mapType(123);
// const oops: never;
oops.toUpperCase(); // compiler error now, oops is never

See that oops is of type never because the compiler knows control flow will never make it to that line.看到oopsnever类型,因为编译器知道控制流永远不会到达该行。 And now this succeeds:现在这成功了:

const report2: FirestoreReport = {
  type: mapType(getSomeNumber()), // okay
  message: "",
  reviewId: ""
}
console.log("YAY " + report2.type);

which is great;这很棒; assuming zod never gives you random things like getSomeNumber() you'll be fine, and otherwise you'll get a runtime error before you make it out of mapType() .假设zod从来没有给你随机的东西,比如getSomeNumber()你会没事的,否则你会在你离开mapType()之前得到一个运行时错误。

Playground link to code Playground 代码链接

Expanding on @jcalz solution, if there's a payload we know virtually nothing about coming from the API and we want to map that to a certain type we can leverage https://github.com/colinhacks/zod to do the following:扩展@jcalz 解决方案,如果有一个有效载荷,我们几乎对来自 API 的信息一无所知,并且我们希望 map 对于某种类型,我们可以利用Z5E056C500A1C4B6A7110B50D807BADEZ 执行以下操作://github.com/

export const APIPayloadSchema = z.object({
  type: z
    .number()
    .refine((type) => Object.values(ReviewReportType).includes(type), {
      message: 'type has to be an allowed number',
    })
    .transform((val) => mapType(val)),
  message: z.string(),
  reviewId: z.string(),
});

export type APIPayload = z.infer<typeof APIPayloadSchema>;

Notice the transform where we call the function that narrows down the number to keyof typeof ReviewReportType请注意我们调用 function 的transform ,它将number缩小到keyof typeof ReviewReportType

We then parse the payload:然后我们解析payload:

const payload = {
  type: Math.floor(Math.random() * 100),
  message: "Foo",
  reviewId: "Bar",
}

const parsed = APIPayloadSchema.parse(report)

And at this point we can safely build the desired object because parsed.type is a keyof typeof ReviewReportType :此时我们可以安全地构建所需的 object 因为parsed.type是一个keyof typeof ReviewReportType

const report: FirestoreReport = {
  type: parsed.type, 
  message: parsed.message,
  reviewId: parsed.reviewId,
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM