dhuCerbin
dhuCerbin

Reputation: 249

How tell typescript that I use conditional type over union correctly?

I have this base case:

type Ids = "foo" | "bar" | "bazz";

interface IFoo {
  foo: string;
}

interface IBar {
  bar: number;
}

interface IBazz {
  bazz: boolean;
}

type Ret<T extends Ids> = T extends "foo" ? IFoo :
  T extends "bar" ? IBar :
  T extends "bazz" ? IBazz :
  never;

function a<T extends Ids>(id: T): Ret<T> {
  switch (id) {
    case "bar":
      return { bar: 1 };
    case "foo":
      return { foo: "foo" };
    case "bazz":
      return { bazz: true };
  }
}

const bar: IBar = a("bar");
const foo: IFoo = a("foo");
const bazz: IBazz = a("bazz");

As you can see, Typescript is not satisfied with my a function implementation. What should I change to compile this function but still keep guarantees in the last three statements?

playground: https://www.typescriptlang.org/play/index.html#code/C4TwDgpgBAkgJgZygXigIgGYHstqgH3QCMBDAJz0LVIC8a0BuAKCYEsA7YCMjEgY2gwAYjigBvJlCjYsALigJgZDgHNmAXxYcuPfoIBC5cZKiky89gFcAtkW4atnbrwGxDdY1No15RHABsIEnYHJlBIKAAlCGAAHgAVKAgADy52RFhEAD4UKESUtIzMHDwAflgRLChZE3zUiHSkanIytyMaqTrCpu9WmHcaapN2CAA3exYMS3Y+YFYsdigSBKT6xsyELIAKVjh5eIBKeWi4+JyJKQQAd1ZgPgALKB24A88pKD4SBGhmig73qRkGKWMiLMSmcjyACMUHUzABn2+6BkaH+AKBwBBYOkOHkxVwsPh70RP16aPeGKx4ghdHkSks0DhJk0miYfAWigh5jaZFyJC2vzQB2Y7PYnJk8mEolQ-PxQpFHOANJ8bQ8MoFvWFQA

Upvotes: 0

Views: 40

Answers (1)

jcalz
jcalz

Reputation: 327754

It's an open issue in TypeScript (see microsoft/TypeScript#33912) that the compiler is generally unable to verify that a particular function return value conforms to a conditional type that depends on an as-yet-unspecified generic type parameter, like Ret<T> inside the implementation of a() where T is unresolved. This is related to the fact that TypeScript is unable to narrow type parameters via control flow analaysis] (see microsoft/TypeScript#24085), so checking id with a switch/case statement might narrow the type of id to, say, "bar", but it does not narrow the type parameter T to "bar", and thus it can't guarantee that Ret<"bar"> is an acceptable output.


One thing you can do is accept that the compiler can't verify this for you and use type assertions or an overload to loosen the implementation typing enough to avoid the errors. This will work, but the compiler is not guaranteeing type safety. For example, with an overload:

function aOverload<T extends Ids>(id: T): Ret<T>;
function aOverload(id: Ids): Ret<Ids> {
  switch (id) {
    case "bar":
      return { bar: 1 };
    case "foo":
      return { foo: "foo" };
    case "bazz":
      return { bazz: true };
  }
}

now there's no error, and there's some type safety... you can't return a completely incorrect type like {spazz: true}, but you can swap the cases around and it won't notice:

function aBadOverload<T extends Ids>(id: T): Ret<T>;
function aBadOverload(id: Ids): Ret<Ids> {
  switch (id) {
    case "bazz":
      return { bar: 1 };
    case "bar":
      return { foo: "foo" };
    case "foo":
      return { bazz: true };
  }
}

So you have to be careful.


Another solution to this particular case is to abandon conditional types in favor of generic indexing, like this:

interface RetMap {
  foo: IFoo,
  bar: IBar,
  bazz: IBazz;
}

function aGood<K extends keyof RetMap>(id: K): RetMap[K] {
  return {
    bar: { bar: 1 },
    foo: { foo: "foo" },
    bazz: { bazz: true }
  }[id];
}

const bar: IBar = aGood("bar");
const foo: IFoo = aGood("foo");
const bazz: IBazz = aGood("bazz");

The compiler is able to verify that what we are doing here is safe, because we're indexing into an object of type RetMap with the id key of type K. Oh, and if you are unhappy that this version preemptively calculates return values it won't use, you could refactor to use getters, which the compiler is also happy with:

function aGood<K extends keyof RetMap>(id: K): RetMap[K] {
  return {
    get bar() { return { bar: 1 } },
    get foo() { return { foo: "foo" } },
    get bazz() { return { bazz: true } }
  }[id];
}

Okay, hope that helps you proceed; good luck!

Playground link to code

Upvotes: 1

Related Questions