Currently, the reverse mapping for numeric enums is not strongly typed and is represented as a numeric index signature whose value type is string. So if you index into a numeric enum with a number key, you will get a string output, as you noticed. 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.
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):
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.
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
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:
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. The return type is in two parts:
The { [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. That operation is to check if the corresponding enum member is assignable to N. If so we return the key, otherwise we return 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. If N is wider, like number, you get all the keys (since each enum member extends 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). If it can, then we want to add an undefined to the output type... otherwise we don't.
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".
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.
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". You mentioned that you're passing stuff through zod (whatever that is ) so the compiler will not know this. The compiler just knows it's some 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. 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:
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:
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. 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().
Playground link to code