Type Widening and Narrowing in TypeScript
Tags: beginners, typescript, webdev
Published: 2021-08-31 12:11:56

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`.

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

// --strictNullChecks: true
let a = null; // type null
let b = undefined; // type undefined
```

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.

```typescript
const num = 2; // type 2;
const name = "Tolu"; // type 'Tolu'
const isTrue = true; // type true
```

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.

```typescript
let surname = "Agboola"; // type string
let num2 = 10; // type number
let bool = true; // type boolean
```

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`:

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

let narrowedName: "Tolu" = name; //narrowed to type 'Tolu';
```

[Enums](https://www.typescriptlang.org/docs/handbook/enums.html) 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:

```typescript
enum Stuff {
	X, // 0
	Y, // 1
	Z, // 2
}
let e = Stuff.X; // type Stuff
const f = Stuff.Y; // type Stuff.Y
```

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](https://github.com/microsoft/TypeScript-New-Handbook/blob/master/reference/Widening-and-Narrowing.md), 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:

```typescript
const nonWideVar: 43 = 43; // type 43
let newVar = nonWideVar; // type 43
```

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:

```typescript
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'.
```

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:

```typescript
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'.
```

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](https://www.typescriptlang.org/docs/handbook/2/narrowing.html), 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!