2

Hello Typescript experts,

can somebody explain why the following Code gives me an error in line 16 but not in 13. Is this intended or a missing feature?

Code

interface Config {
  // There need to be different types in here for the error to occur
  A: number
  B: string
}

type union = "A" | "B" 

var Global = {A: 1, B: "Hello"} as Config

function foo<K extends union>(key: K, x: union) {
  if (x === "A"){
    Global[x].toFixed()
  }
  if (key === "A"){
    Global[key].toFixed()
  }
}

Playground Link

Ru Chern Chong
  • 3,692
  • 13
  • 33
  • 43
Yugon
  • 23
  • 3

2 Answers2

2

It's probably a missing feature.

When you check (x === "A") it acts as a type guard on the type of the value x, causing it to narrow from the union type "A"|"B" to just "A" via control flow analysis.

Unfortunately, generic type parameters in TypeScript don't get narrowed via control flow analysis; see microsoft/TypeScript#24085 for more information. So, for example, checking (key === "A") will not narrow the type of K to "A". This sort of restriction makes sense when you have multiple values of the same generic type:

function foo<K extends Union>(key: K, x: Union, key2: K) {    
  if (key2 === "A") {
    Global[key].toFixed(); // error!
  }
}

Obviously checking the value of key2 should have no effect on the type of key, so the compiler conservatively does not ever think K should be narrowed. This is the basic problem in microsoft/TypeScript#13995 and multiple related issues have been raised with suggestions about how to handle it for the cases where it should be safe to do such narrowing. So far nothing has made it into the language, though.


This isn't really the full story, though; one could counter with: okay, maybe you can't narrow the type parameter K from K extends Union to K extends "A", but surely you can narrow the type of the value key from K to "A" or K & "A" (an intersection type), which would make Global[key].toFixed() succeed:

if (key === "A") {
  Global[key as (K & "A")].toFixed(); // okay
}

And I don't have a good answer to that right now. Most questions I've seen about this end up being referred eventually to microsoft/TypeScript#13995. ‍♂️

The closest I can come to a full answer is that it seems that using built-in type guards like a === b or typeof a === "string" or a instanceof B or a in b only ends up filtering unions or possibly narrowing string or number to string or number literals, but never produces an intersection type. I've asked before, see microsoft/TypeScript#21732, for the a in b type guard to produce some intersections, but it hasn't been implemented. So this might be two missing features:

  • no narrowing generic type parameters, and
  • no built-in type guard narrowing to intersections.

So, workarounds: Obviously the easiest one for this example is just to reassign the generic-typed variable's value to a union-typed variable:

const k: Union = key;
if (k === "A") {
  Global[k].toFixed();
}

Or, you could use a type assertion as in the above as (K & "A") or just as "A":

if (key === "A") {
  Global[key as (K & "A")].toFixed(); // okay
  Global[key as "A"].toFixed(); // okay
}

Or, if this happens a lot, you could write your own user-defined type guard function, since user-defined type guards do produce intersections in the true branch of the subsequent control flow:

const isEq =
  <T extends string | number | boolean>(v: any, c: T): v is T => v === c;

if (isEq(key, "A")) {
  Global[key].toFixed(); // okay, key is now of type K & "A";
}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

I'm not sure why that error occurs, but I'm sure someone has a good explanation. Here is an alternative way of achieving the same by using intersection types with generics:

interface Config {
  // There need to be different types in here for the error to occur
  A: number
  B: string
}

type Union = keyof Config;

const Global: Config = { A: 1, B: "Hello" };

function foo<K>(key: K & Union, x: Union) { // Use intersection type instead of inheritance
  if (x === "A"){
    Global[x].toFixed();
  }
  if (key === 'A'){
    Global[key].toFixed()
  }
}

Playground link

Also see this question: Difference between extending and intersecting interfaces in TypeScript?

smac89
  • 39,374
  • 15
  • 132
  • 179