目前,
数字枚举的反向映射没有强类型化,表示为一个数值
索引签名,其值类型为
string
。因此,如果您使用
number
键索引数字枚举,您将得到一个
string
输出,正如您所注意到的那样。在您传入有效的枚举成员时,这种类型过于广泛。
const str: "HARASSMENT" = ReviewReportType[ReviewReportType.HARASSMENT];
// ^^^ <
这是
microsoft/TypeScript#38806的主题,目前正在等待更多社区反馈的功能请求。
此外,在传递无效成员的情况下,它的类型范围太窄了,因为它没有预测到可能的
undefined
(除非您打开
--noUncheckedIndexedAccess
编译器选项,大多数人不会这样做,并且它不是
--strict
编译器选项套件的一部分):
const oops = ReviewReportType[123];
oops.toUpperCase();
如果你想编写一个
mapType()
函数,以“正确”的方式考虑这两个因素,你可以...但是由于编译器不会为你处理它,所以你需要使用
type assertion来告诉编译器
ReviewReportType[type]
实际上是你声称返回类型的类型。
需要注意的是,我们可以只使用带有类型断言的您的版本,如下所示:
const mapType = (num: number) => ReviewReportType[num] as keyof typeof ReviewReportType;
但它有非常相似的限制...你会得到
typeof ReviewReportType
的
keyof
,而不是
string
,但它仍然太宽泛了。
const str: "HARASSMENT" = mapType(ReviewReportType.HARASSMENT); // still error
"太窄了"
const oops = mapType(123);
// const oops: "HARASSMENT" | "INAPPROPRIATE" | "UNKNOWN_PERSON" | "FAKE_REVIEW" | "OTHER"
oops.toUpperCase(); // still no compiler error but RUNTIME ERROR!
所以你需要小心处理它。
相反,我会写一个
通用版本的
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)
这有点难以理解,但我会尽力解释。该函数是在
N
中泛型化的,
num
是
number
-
constrained类型。返回类型由两部分组成:
{ [P in keyof typeof ReviewReportType]: typeof ReviewReportType[P] extends N ? P : never }[keyof typeof ReviewReportType]
是一个 分布式对象类型(在 ms/TS#47109 中所称),它立即 索引 到一个映射类型中,以便将类型操作分配到ReviewReportType
枚举键的联合上。该操作是检查相应的枚举成员是否可分配给N
。如果是,则返回键,否则返回never
。因此,如果N
为6
,则当键为"HARASSMENT"
时,它将是"HARASSMENT"
,否则将是never
。所有这些的并集就是我们想要的"HARASSMENT"
。如果N
更宽,比如number
,你会得到所有的键(因为每个枚举成员都扩展了number
)。
(`${N}` extends `${ReviewReportType}` ? never : undefined)
部分用于检查N
是否可以失败成为枚举成员(我需要使用模板文字类型来做这件事,因为数字枚举被认为比相应的数字文字类型更窄;将两侧转换为string
文字可以规避这个问题)。如果可以,则我们希望在输出类型中添加一个undefined
...否则不需要。
把这两个结合起来,你就可以得到我能够得到的最接近准确行为。
const str: "HARASSMENT" = mapType(ReviewReportType.HARASSMENT); // okay
现在这个可行是因为
mapType(ReviewReportType.HARASSMENT)
返回
"HARASSMENT"
。
const oops = mapType(123);
// const oops: undefined
oops.toUpperCase(); // compiler error now, oops is undefined
这是一个编译错误,因为
mapType(123)
返回
undefined
。
现在我们可以根据需要使用它:
const report: FirestoreReport = {
type: mapType(ReviewReportType.HARASSMENT),
message: "foo",
reviewId: "bar"
}
这个成功的原因是编译器知道
ReviewReportType.HARASSMENT
是
6
,而
mapType(6)
是
"HARASSMENT"
。你提到了你正在通过
zod
(无论它是什么)来传递信息,所以编译器不会知道这一点。编译器只知道它是一个
数字
。
function getSomeNumber(): number {
return Math.floor(Math.random() * 100);
}
"所以你会得到一个错误: "
const report2: FirestoreReport = {
type: mapType(getSomeNumber()),
message: "",
reviewId: ""
}
我认为这是正确的行为。如果编译器无法验证您在那里未分配undefined,则
应该向您发出警告。您可以通过使用
非空断言运算符(!
)来修复此问题:
const report3: FirestoreReport = {
type: mapType(getSomeNumber())!,
message: "",
reviewId: ""
}
但是...也许你讨厌那个?
如果是这样的话,这就是我的最终提议。如果返回值将是undefined,则使函数抛出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];
};
现在你知道,在调用
mapType()
后运行的任何代码都会有枚举的某个键:
const str: "HARASSMENT" = mapType(ReviewReportType.HARASSMENT);
const oops = mapType(123);
oops.toUpperCase();
看到
oops
的类型是
never
,因为编译器知道控制流永远不会到达那一行。现在这个成功了:
const report2: FirestoreReport = {
type: mapType(getSomeNumber()),
message: "",
reviewId: ""
}
console.log("YAY " + report2.type);
这是很好的;假设
zod
不会给你随机的东西,比如
getSomeNumber()
,那么你就没问题了,否则在你离开
mapType()
之前就会收到运行时错误。
代码操场链接
undefined
作为可能的结果。这种方法 是否满足你的需求?如果是,我可以撰写一个答案;如果不是,那么我缺少什么? - jcalz