skip to Main Content

I am learning TS and have the following code:

function getOrDefault<T = string>(value: T, defaultValue: T, car: T): T {
    return value || defaultValue;
}

const strValue = getOrDefault("Hello", "Default", "o");
const numValue = getOrDefault(undefined, 42, "o"); 

The TS Playground compiler complains about the numValue variable because Argument of type '"o"' is not assignable to parameter of type '42'. whereas the strValue doesn’t complain.

When I hover over the first function call, the type is "Hello" | "Default" | "o", in the 2nd call its only showing 42 | undefined. If I replace "o" with a number then the error goes away and it appears in the union list as well.

I’m wondering what does the error mean about "o" and the parameter of type 42, why isn’t it added into the union list as undefined and 42 are? Any simple explanation to understand this would be appreciated.

2

Answers


  1. In your generic, all three arguments are of type T

    But for your numValue, you are providing a number for the second parameter and a string for the third.

    The type system is telling you that a number and a string are not the same type.

    Login or Signup to reply.
  2. TypeScript uses various heuristic rules to determine when to synthesize union types when faced with multiple inference candidates. In general this is just prohibited; a call signature of type <T>(x: T, y: T)=>void will tend to reject calls like f(x, y) where x is of type X and y is of type Y, instead of synthesizing the union X | Y and accepting it. This is described in microsoft/TypeScript#49850 and links from there, and Why isn't the type argument inferred as a union type?.

    But there are exceptions to this.

    One is if X or Y are null or undefined. Before the introduction of --strictNullChecks in TS2.0, null and undefined were considered assignable to all types, and thus they were implicitly accepted in calls like f("abc", undefined). Since then they are now considered to be distinct types, but they are still allowed in calls like the above. So unions with null and undefined are synthesized more eagerly.

    Another exception involves literal types like "a" or 1 or true. Before TypeScript introduced string literal types in TS1.8 as implemented in microsoft/TypeScript#5185 and numeric and boolean literal types as implemented in microsoft/TypeScript#9407, A call like f("a", "b") or f(1, 2) or f(true, false) would be accepted because T was just string, number, or boolean. But now that literal types exist, a straight reading of "don’t synthesize union types" would mean those might be rejected. But nobody wants that to be rejected. So even if literal types are inferred, literal types of the same widened type will be accepted and a union of them synthesized. So you’ll get "a" | "b" or 1 | 2 or true | false (which is just boolean).


    That’s the answer to the question as asked. If you think it should be possible to ask TypeScript to synthesize unions in situations like this then you might want to give an upvote to microsoft/TypeScript#44312. Until then you just need to work around it. The easiest workaround is to just add more generic type parameters and synthesize their union yourself:

    function getOrDefault<T, U, V>(value: T, defaultValue: U, car: V): T | U | V {
      return value || defaultValue;
    }
    
    const strValue = getOrDefault("Hello", "Default", "o"); // okay
    // const strValue: "Hello" | "Default" | "o"
    
    const numValue = getOrDefault(undefined, 42, "o"); // okay
    // const numValue: "o" | 42 | undefined
    

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search