Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In typescript, is there a way to ensure that a function's return type is exhaustive?

Tags:

typescript

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?

like image 795
Ziggy Avatar asked Mar 05 '23 15:03

Ziggy


2 Answers

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!

like image 160
jcalz Avatar answered Apr 09 '23 15:04

jcalz


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);
  }
}
like image 30
AlesD Avatar answered Apr 09 '23 15:04

AlesD