Search code examples
typescripttypesextendsunion-typesconditional-types

Why does 0 | "" | {} extends 0 | "" ? true : false returns false


0 | "" | {} extends 0 | ""  // false
0 | "" | {} extends 0 | {} // true

The union 0 | "" | {} is larger than the union 0 | "" so it technically extends from it. I am puzzled as to why it returns false.

This question is from the answer to a Typescript type-challenge AnyOf. I would appreciate if someone could breakdown the answer for me. In particular I don't understand how Typescript computes UnionA extends UnionB to determine whether it is true or false.


Solution

  • The keyword was chosen to match ES6 classes: class SubClass extends BaseClass... which means SubClass is a more specialized type.

    Knowing this, one problem then become clear:
    0 | "" | {} is not a more specialized type than 0 | "". Instead of narrowing it down, you added more possibilities for it to handle. So it should indeed return false.

    You can test the idea of "MoreSpecific extends LessSpecific", for example: 0 | "" extends 0 | "" | {} ? true : false (result: true).

    Another approach is to think about assignments.
    let variable: BaseClass = some_instance_of_SubClass should be a valid assignment.
    But if you have a value known to be of type 0 | "" | {} and try let myVar1: 0 | "" = value; you'll find that it's not assignable, as none of the members of the union type 0 | "" allows you to assign {}.

    As for 0 | "" | {} extends 0 | {} leading to true, it’s because of some peculiarities of the empty object type. You can do such assignments:

    let test1: {} = ""
    let test2: {} = 34
    let test3: {} = {"anything you like": "other than null or undefined"}
    

    (it is apparently done for some historical purposes, see https://github.com/microsoft/TypeScript/issues/44520 “It's a concession to back-compat because { } was the top type when generics were introduced”)

    Since you can assign anything other than null or undefined to {}, anything other than null or undefined extends {}.
    0 | "" | {} is assignable to type {} as none of the values are null/undefined. Therefore, 0 | "" | {} is assignable to type 0 | {}. So "... extends ..." should return true.

    The 'extra possibility' of "" is already covered by the {} part of 0 | {}.