Type Widening in TypeScript

Notes from Effective TypeScript

What exactly is type widening?

Type widening is the process by which the type checker assigns a type to a variable initialized with a constant value when no type annotations are provided.

Example: Given

let x = 'v'

When type widening is done x will have an inferred type of 'string'.

Why is it important to understand type widening?

So that we understand errors that are thrown due to unassignable values caused as a result of type widening.

Example: Given

let x = 'banana'

let fruit: 'orange'|'banana' = orange
fruit = x;
> error: cannot assign string to type of 'orange'|'banana'

Without an awareness of type widening, we would struggle to understand why a string looking variable, fruit, cannot be assigned the value of another seemingly string looking variable, x. With an awareness of type widening, we can understand that type of fruit is a much more specific type than that of string and following structural typing cannot permit an assignation of type string.

If you're struggling with structural typing remember it as the process that allows subsets to be assigned to supersets, and be careful in how you determine which the subset is and which the superset is. For instance, subsets can 'look' bigger than supersets, but typically a subset has more properties defined than a superset, or put another way, {} is a very large set while {x: string} is also a very large set but smaller than {}.

How does structural typing (subsets to superset types) let typescript widening errors occur?

It only allows narrower to wider type assignations, but not wider to narrower assignations, so in the case where a constant's type in inferred as, say, string, assigning that type to a narrower type of 'banana' | 'apple' causes an error with respect to structural typing.

Is there any way to predict the outcome of type widening?

Yes. Although very much an ambiguous process, we can predict the outcome of a widened type depending on the following:

1. If we used let or const

Let leads to wider inferences than const which typically only allows for the very specific type of that value.

Example:

let x = 'x' > type string
const x = 'x' > type 'x'

2. If we used the as const annotation

Different from the const keyword which exists in value space to declare constant variables, as const chooses the most specific type when used, and is most useful in controlling the types of object properties.

Example:

  const obj = {

      a: 'x' as const
      b: 'sugar'
  }

Without as const, property 'a' would have been inferred as 'string.' With as const, it is inferred as having type 'x'.

Similarly,

  const obj = {

      a: 'x',
      b: 'sugar'
  } as const

Without as const, obj would have been inferred as obj: {a: string, b: string}. With const it is inferred as obj: {readonly a: 'x', readonly b: 'sugar'}.

3. If we provided a type declaration annotation

Providing a type annotation narrows the type inference to the type provided.

Example

  let x: 'orange' = banana > throws an error, string cannot be assigned to type 'orange'

4. If it is an element of an object

Elements of objects are treated as though having been declared using the let keyword.

5. If we declared the object all at once or in bits

Type widening only allows for inference on elements present in an object at declaration time. It does not allow for new object properties.

Give an example of the rule: a variable's type shouldn't change after it's declared

let x = 'orange'
x = 1

Typescript will constrain a constants type to limit it to the set of values that can be assigned to that variable without needing to change the type of the variable. So an assignation like the one above where we're trying to assign an entirely different type of 'number' to a type of 'string' will throw an error.

Give another rule governing how TS treats properties of objects.

Elements of objects are treated as though having been declared by let

What is a significant limitation imposed on objects as a result of this rule

You cannot add new properties which were not originally part of the object when it was declared.

List 4 methods to control the TS widening process.

  • Use const
  • Use as const
  • Use a type declaration annotation
  • Declare objects in full

Which of these apply to single value variables and which to objects and tuples? Give examples

They all apply to both, except perhaps the last method. The as const method applies to objects and tuples in particular.

Using as const on an array like object is great for creating a tuple type.

Example:

let tuple = [1,2] as const
> type is inferred as readonly [1 , 2]

compared to

let tuple = [1,2]
> type is inferred as number[]