If you have a const enum like
enum Color {
RED,
GREEN,
BLUE,
}
You can write a helper and a switch statement,
function assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}`)
}
function toString (key: Color): string {
switch (key) {
case Color.RED: return 'Red'
case Color.GREEN: return 'Green'
case Color.BLUE: return 'Blue'
default: return assertNever(key)
}
}
such that if we ever change Color
, we must change our toString
implementation.
However, if I go the other way,
function fromString (key: string): Color {
switch (key) {
case 'Red': return Color.RED
case 'Green': return Color.GREEN
case 'Blue': return Color.BLUE
default: throw new Error(`${key} is not a Color`)
}
}
It is possible that my fromString implememtation may get out of date with my Color enum.
Is there a way to ensure that there exists some path that returns each kind of Color
? Is there are way to ensure that the range of the function is Color
?
There's no built-in functionality which will enforce this for you automatically. It is not considered an error for the actual return type of a function to be more specific than the declared return type... if a function is declared to return a string
but actually always returns the particular string "hello"
, that's fine. It's only an error to do the reverse, where the function is declared to return the particular string "hello"
but actually returns a general string
.
One thing you can do to achieve something like this in general is to let TypeScript infer the return type of a function and then use a compile-time check to make sure it is what you think it is. For example:
// MutuallyExtends<T, U> only compiles if T extends U and U extends T
type MutuallyExtends<T extends U, U extends V, V=T> = true;
// note how the return type is not annotated
function fromString(key: string) {
switch (key) {
case 'Red': return Color.RED
case 'Green': return Color.GREEN
case 'Blue': return Color.BLUE
default: throw new Error(`${key} is not a Color`)
}
// the next line will error if not exhaustive:
type Exhaustive = MutuallyExtends<ReturnType<typeof fromString>, Color>
}
The above compiles, but the following produces an error because Color.BLUE
is missing:
function fromString(key: string) {
switch (key) {
case 'Red': return Color.RED
case 'Green': return Color.GREEN
default: throw new Error(`${key} is not a Color`)
}
type Exhaustive = MutuallyExtends<ReturnType<typeof fromString>, Color> // error!
// Color does not satisfy constraint Color.RED | Color.GREEN ---> ~~~~~
}
This is, of course, a workaround. But maybe it will help you or others. Hope that is of some use; good luck!
There is a possible workaround solution that might work for you don't know if I understand correctly what you want to achieve.
You can define a string literal type with all possible color strings. Then when you change the enum you will first need to change the toString
function which will force you to add another value to the color names type because you will have no value for the new color. This will then break the fromString
function so you will need to update it for the build to work. This is how the code looks like with the change:
enum Color {
RED,
GREEN,
BLUE
}
type ColorName = 'Red' | 'Green' | 'Blue';
function assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}`);
}
function toString (key: Color): ColorName {
switch (key) {
case Color.RED: return 'Red';
case Color.GREEN: return 'Green';
case Color.BLUE: return 'Blue';
default: return assertNever(key);
}
}
function assertNeverColor(x: never): never {
throw new Error(`${x} is not a Color`);
}
function fromString (key: ColorName): Color {
switch (key) {
case 'Red': return Color.RED;
case 'Green': return Color.GREEN;
case 'Blue': return Color.BLUE;
default: return assertNever(key);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With