DEV Community

Cover image for Type Widening and Narrowing in TypeScript
Toluwanimi Isaiah
Toluwanimi Isaiah

Posted on • Edited on

Type Widening and Narrowing in TypeScript

There are many important concepts to understand to be able to write TypeScript effectively. This article will discuss two of them: Type widening and narrowing. Widening and Narrowing types is about expanding and reducing the possibilities which a type could represent.

Type Widening

To represent an absence of something, JavaScript has two values: null and undefined. These are special because in TypeScript, the only thing of type null is the value null, and the only thing of type undefined is the value undefined. let variables initialized to null and undefined are widened to any if strict null checking is off. To make sure the types remain the same, in your tsconfig.json file, set --strictNullChecks to true.

// --strictNullChecks: false
let a = null; // type any
const b = null; // type null

// --strictNullChecks: true
let a = null; // type null
let b = undefined; // type undefined
Enter fullscreen mode Exit fullscreen mode

Note that the compiler widens on assignment. null is still of type null, and undefined is still of type undefined.

Literal widening

Literal widening in TypeScript is when a literal type gets treated as its base type. When you declare a variable using the const keyword and initialize it with a literal value, TypeScript will infer a literal type for that variable. TypeScript knows that once a primitive is assigned with const its value will never change, so it infers the most narrow type it can for that variable.

const num = 2; // type 2;
const name = "Tolu"; // type 'Tolu'
const isTrue = true; // type true
Enter fullscreen mode Exit fullscreen mode

However, when you declare a variable with let or var, you're telling Typescript that its value can be changed later. So its type is inferred to be the base type that its literal value belongs to.

let surname = "Agboola"; // type string
let num2 = 10; // type number
let bool = true; // type boolean
Enter fullscreen mode Exit fullscreen mode

In the above example, surname is initialized with the let keyword, so TypeScript gave it a wider type of string therefore giving it a wide set of possibilities. If Typescript were to infer literal types for let variables, trying to assign a different value from the inferred literal would cause an error at compile time.

Now, when a narrow type is reassigned to a mutable location e.g a let variable, the new variable will be widened to and treated as its respective widened type. For example, if you assign the value of the constant name to a mutable variable widenedName, the type of widenedName will be the base type of name which is type string:

let widenedName = name; // type string
let widenedNum = num; // type number

let narrowedName: "Tolu" = name; //narrowed to type 'Tolu';
Enter fullscreen mode Exit fullscreen mode

Enums are a way to define a set of named constants and enumerate the possible values for a type. They are unordered data structures that map keys to values, similar to objects. The values of enum members are auto incremented unless specific values are assigned to them. Take the following enum:

enum Stuff {
    X, // 0
    Y, // 1
    Z, // 2
}
let e = Stuff.X; // type Stuff
const f = Stuff.Y; // type Stuff.Y
Enter fullscreen mode Exit fullscreen mode

The mutable variable e to which enum member X is assigned to is widened to type Stuff, the containing enum of X. However, when const is used, TypeScript narrows the type of f to the member of the enum itself, Stuff.Y.

What literal types widen?

According to this handbook, literal types widen to their respective supertype:

  • Number literal types like 1 widen to number.
  • String literal types like 'hi' widen to string.
  • Boolean literal types like true widen to boolean.
  • Enum members widen to their containing enum.

Non-widening literal types

You can prevent your type from being widened with explicit annotation:

const nonWideVar: 43 = 43; // type 43
let newVar = nonWideVar; // type 43
Enter fullscreen mode Exit fullscreen mode

This way, even if you reassign the value to a mutable location, the type will remain narrow.

Type Narrowing

TypeScript provides different ways to combine types. One of them is Union type. A union type is formed from two or more other types, and represents a value that can be any one of them. To understand narrowing, you need to know what declared type and computed type mean. The declared type of a variable is the one it is declared with, while the computed type varies based on context. Consider the following code:

let strNum: string | number; // type string | number (declared type)

strNum = "var"; // OK
strNum = 10; // OK
strNum = false; // Error: Type 'boolean' is not assignable to type 'string | number'.
Enter fullscreen mode Exit fullscreen mode

You can assign either a string or a number to strNum without any complaints from TypeScript because the possibilities of its values have been specified in the union type. However, if you assign any other type of value to it, the compiler will give an error.

Function parameters can also be of union type:

function logType(val: string | boolean) {
    if (typeof val === "string") {
        console.log("Value is a string"); // Computed type here is string
    } else if (typeof val === "boolean") {
        console.log("Value is boolean"); // Computed type here is boolean
    }
}

logType("Value"); // OK
logType(true); // OK
logType(10); // Error: Argument of type '10' is not assignable to parameter of type 'string | boolean'.
Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at the logType function above. It receives one argument, val, that can either be a string or boolean. The first block of code within the function checks the type of the argument and will only run if the computed type of the argument is a string. It basically narrows the type of val to string allowing it to be temporarily treated as a string within that context. The same thing is happening within the second block which narrows the argument to boolean. Outside of those contexts, the type remains string | boolean. Narrowing, which is the removal of types from a union, has just taken place.

Conclusion

In this article, we've seen how and why TypeScript widens the types of mutable variables, how strict null checking affects widening of null and undefined values. We saw what types can be widened, and how to prevent widening with explicit annotation. We also discussed what declared types and computed types mean, and how they play a part in type narrowing.

I hope you have gained some value from this explanation. Let me know your thoughts in the comments.

Thanks for reading!

Top comments (2)

Collapse
 
middleverse profile image
Arshi Bhasin • Edited

Something doesn't make sense:

"Note that the compiler widens on assignment. null is still of type null, and undefined is still of type undefined." -> This should be "Note that the compiler [doesn't widen] narrows on assignment...".

Collapse
 
middleverse profile image
Arshi Bhasin

Nvm, since it's in the context of initialization to type inference, it can be regarded as widening, but the labeling is trivial in this case. Good article btw!