Typescript: ensure it's possible function can return every value in union type

Viewed 118

I have a function that returns a value of a union type based on some input.

I want Typescript to check it's theoretically possible for the function to return every value from the union type. Or stated differently, that's it's not impossible for any particular value to be returned.

type Thing = 'a' | 'b' | 'c'

// should pass since it's possible for all of 'a', 'b', 'c' to be returned
const pass = (input: number): Thing => {
  if (input === 1) return 'a'
  if (input === 2) return 'b'
  return 'c'
}

// should fail since it's impossible for 'c' to be returned no matter the input
const fail = (input: number): Thing => {
  if (input === 1) return 'a'
  return 'b'
}

To help clarify, the inverse pattern of checking every case of an input union type, with a switch, looks like the following. I want the same behaviour, but for output 'cases' instead of input.

const example = (input: Thing) => {
  switch (input) {
    case 'a': return 1
    case 'b': return 2
    default: {
      let x: never = input // fails because 'c' not covered
    }
}

This definitely seems like something Typescript should be able to figure out. I'm thinking the solution should be, basically, a way to say 'this function returns a union type that is exactly equal to the union described by Thing, not merely a subset of it', but I'm not sure if / how that can be expressed in Typescript.

1 Answers

The default behavior is to allow the assignment of a function that returns "a" | "b" to a function reference that return "a" | "b" | "c", after all if the caller can handle the wider type it can also handle the more narrow without any ill effects.

You can use an extra function for checking that the inferred returned type is the same as the type you expect. You need to use a conditional type to trigger a sort of custom error for this case.


type IsSame<A, B, Y, N> = [A] extends [B] ? [B] extends [A] ? N : Y :Y

function checkReturnType<T>() {
    return function <R>(fn: ((...a: any[]) => R) & IsSame<T, R, ["Bad return type expected", T, "got", R], unknown>) {
        return fn
    }
}

// fails since it's impossible for 'c' to be returned no matter the input
const fail = checkReturnType<Thing>()((input: number) => {
    if (input === 1) return 'a'
    return 'b'
});

Playground Link

Related