TypeScript: Generic index access on target type and return based on discriminated types do not work.

TypeScript Version: ^3.5.0

Search Terms: index access target type discriminated types

Code

interface TypeA {a: string; }
interface TypeB {b: string; }

class A implements TypeA {public a: string = ""; }
class B implements TypeB {public b: string = ""; }

enum Types {
  A= "a", B = "b",
}

interface TypeInterfaces {
  [Types.A]: TypeA;
  [Types.B]: TypeB;
}

function create<T extends Types>(type: T): TypeInterfaces[T] {
  if (type === Types.A) {
    return new A;
  } else if (type === Types.B) {
    return new B;
  }

  throw new Error();
}

Expected behavior: The compiler knows with discriminated types exactly what it needs to return and there are no errors as in 3.4 or lower.

Actual behavior: The compiler gives an error that we cannot assign the returned A or B to TypeA & TypeB regardless of the fact that we actually know for sure with discriminated types that we need to return either TypeA or TypeB.

Playground Link: Try it on playground

Related Issues: This issue was specifically introduced here for v3.5 https://github.com/microsoft/TypeScript/pull/30769 by @ahejlsberg when it was decided to have the target type on index types as intersections.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 22 (3 by maintainers)

Most upvoted comments

@fatcerberus Even a lower bound would not be sufficient I think. A truthy type test of typeof x === "string" does not rule out T = "some string literal", so a lower bound of string would be incorrect. I don’t think there is anyway to get the right behaviour with bounding, and you really need a new predicate like: #25879, #27808, #28430.

TS doesn’t have any kind of exhaustiveness checking AFAIK so if it were a union instead of an intersection, nothing would stop you from returning the wrong thing in an else-clause, for example. Prior to TS 3.5 (when it was a union) you could just replace your entire function with return new A; and it would typecheck fine (try it in the playground!). And then be wrong at the call site that asked for a B.

Trust me—to make this work you need some way to narrow T, which isn’t currently possible. 😢

@archfz Right, that’s why I said it gets more complicated with generics - you can’t narrow a type parameter. I was merely explaining where the intersection was originating from.

This approach will not scale once the constructors need arguments that must be supplied to create.

class Factory {
  get [Types.A]() {
    return new A();
  }

  get [Types.B]() {
    return new B();
  }
}

const f = new Factory();

function create<T extends Types>(key: T) {
  return f[key];
}

const a: A = create(Types.A);