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?
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>>
]
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