Search code examples
typescripttypeof

Widening behaviour when using typeof


I'm trying to understand the behaviour of TypeScript when widening certain literal types. I have created a code example to explain: Playground.

I have 4 examples of literal types that I assign to let variables:

let A = 1;
let B = 'blah';
let C = true;
let D = Shapes.Rectangle;

If you hover over each let in the playground you can see that the literal type has been widened to its broader type as expected for all four types. However, when combined with typeof in the tests, only A and B widen to their broader types whereas C and D do not.

Can anyone explain this behaviour?


Solution

  • Typescript compiler needs to statically decide the type of variable that should not change throughout the program.

    Hence when it is not sure of the exact type it defaults to the closest wider type that it can resolve to.

    As let declaration can change somewhere in our execution down the line, it makes a pragmatic guess and assigns its type of number The same happens with boolean, string, and other enum types

    With let is possible that somewhere else we can reassign D as Shapes.Square So in that case we don't want our types to be out of sync and behave randomly!

    But if we make it const, we are guaranteed that they won't change throughout the execution of this program Hence it is safe for the typescript compiler to infer the literal assigned type

    In case you want the types to be the actual types and not the widened ones, you can always annotate it so! let D: Shapes.Rectangle = Shapes.Rectangle

    import type { Equal, Expect } from '@type-challenges/utils'
    
    enum Shapes {
      Square = 'square',
      Rectangle = 'rectangle'
    }
    
    const A = 1; 
    const B = 'blah';
    const C = true;
    const D = Shapes.Rectangle;
    
    type tests = [
      Expect<Equal<typeof A, 1>>,
      Expect<Equal<typeof B, 'blah'>>,
      Expect<Equal<typeof C, true>>,
      Expect<Equal<typeof D, Shapes.Rectangle>>
    ]
    

    Code Playground

    Now coming towards your question that why Typescript narrows some of the types (boolean, Enum), whereas widens the other ones (number, string)

    The simple reason for that is Typescript compiler is smart enough to check that if the Type set contains finite type elements then it can infer it based on the usage.

    Ex 1: type boolean = true | false // finite set

    Ex 2: enum Shapes { Square = 'square', Rectangle = 'rectangle'} // finite set

    Here we defined FiniteTypes alias which constitutes of number | string | any[] Hence when we reassign values to any of these types, typeof is sure of the type that is currently in the variable as it can loop over the constituents

    Whereas in the case of types which are number, string, etc there are infinite constituents for these and hence we don't narrow down the type based on its reassignment.

    Example

    // Proof that TS compiler is smart enough to loop through Finite elements of type
    
    type FiniteTypes = number | string | any[]
    
    let z: FiniteTypes = 2
    type t_1 = typeof z // Inferred as number
    
    z = [1,2,3]
    type t_2 = typeof z // Inferred as any[]
    
    z = "Foo bar"
    type t_3 = typeof z // Inferred as string
    
    
    

    Example for narrowing types based on Assignment