[英]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 在
N
的number
约束类型中是通用的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.所以如果
N
是6
,那么当键是 " "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.HARASSMENT
是6
并且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.看到
oops
是never
类型,因为编译器知道控制流永远不会到达该行。 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()
之前得到一个运行时错误。
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.