Type Narrowing and Type Widening

In this chapter, the focus will be on the essentials of TypeScript's type system. We will focus on the data types that you can also find in JavaScript, which can be separated into literal types and collective types. Collective types allow for flexibility in defining variables that can hold more than one value, providing a broader range of possibilities for variable assignments. On the other hand, literal types bring precision to how values are represented, ensuring that they adhere to specific, predefined values rather than being a general type.

Afterwards, we will explore how TypeScript uses this information to narrow or widen a type. Type narrowing is a powerful feature that refines types based on the context in which they are used, making it easier to handle specific scenarios. Type widening ensures broader compatibility, allowing more flexibility when working with different types.

Collective Types

A collective type represents a set of multiple possible values. For example, the number type is collective because it can represent values like 1, 2, 42, or even -99. Similarly, the string type is collective because it can represent various strings like "Hello", "World", or any other word.

Example of Collective Types

// Represents any number
let age: number;
age = 30;
// Both are valid values for the type 'number'
age = -5;

Visualizing this concept can help clarify it. Think of the number type as a collection containing all possible numeric values, and the string type as a collection of all possible textual values.

Collective Type Examples


Literal Types

Unlike collective types, literal types represent specific values within a type. For example, a string literal type can be "Hello", which only allows the exact value "Hello" and no other string. Similarly, a number literal type like 42 restricts the variable to only hold that value.

Example of Literal Types

// Only "Hello" is allowed here
const greeting: 'Hello' = 'Hello';

You can define literal types for string, number, boolean, and even objects, making them highly useful for strict type-checking in your code.


Type Narrowing

TypeScript’s type narrowing is the process of refining a broader type to a more specific type. This often happens when you provide additional information about a variable, such as assigning a specific value or using conditional logic.

By default, TypeScript assigns the any type to variables without explicit type information:

// Implicitly 'any'
let text;
text = 'Hello';

To avoid the implicity any error (TS7005), you can annotate the type explicitly:

// Now 'text' must always be a string
let text: string;
text = 'Hello';

We can take narrowing a step further by using a literal type:

// The only valid value is "Hello"
const greeting: 'Hello' = 'Hello';

Type Widening

Type widening occurs when TypeScript automatically generalizes a specific type into a broader type. For example, if you declare a variable with a literal value like a string or number, TypeScript typically widens the type to its corresponding primitive type (e.g., from the string literal type "Hello" to the broader type string):

const specificGreeting = 'Hello'; // Type inferred as "Hello"
let generalGreeting: string = 'Hello'; // Widened to 'string'

TypeScript’s primary goal is to enable both flexibility and safety. By widening types, it allows variables to hold multiple values of a general category, such as any string or number, instead of locking them into a single specific value.

Without widening, you would need to manually annotate every variable to ensure they could store broader values, which would make TypeScript unnecessarily restrictive and cumbersome.

Illustration:

Type Widening


Control Flow-Based Type Analysis

TypeScript’s type checker doesn’t stop at static declarations; it can analyze the flow of your code to refine types dynamically. This is called control flow-based type analysis. This analysis helps TypeScript to automatically narrow a collective type:

function reply(text: 'Hello' | 'Bye') {
  if (text === 'Hello') {
    // The type of "text" is now "Hello"
    return text;
  } else {
    // The type of "text" is now "Bye"
    return 'See you!';
  }
}

Here, TypeScript detects that text is narrowed to the string literal type "Hello" inside the if block.


What You Have Learned

Understanding Collective and Literal Types: You have learned the difference between collective and literal types in TypeScript. Collective types, like number or string, represent a range of possible values. Literal types, on the other hand, are specific values within a type, such as "Hello" or 42, which restricts variables to hold only those exact values.

Type Narrowing: You now understand how TypeScript’s type narrowing works. By using explicit type annotations or conditional logic, you can refine a broader type into a more specific type. For example, you can narrow a string to a literal type like "Hello" to ensure a variable only holds that exact value.

Type Widening: You have learned how TypeScript performs type widening, automatically generalizing a specific type to a broader one. For instance, a string literal like "Hello" is widened to the general string type. This provides flexibility, allowing variables to hold multiple values of a broader category, like any string or number.

Control Flow-Based Type Analysis: You now understand how TypeScript’s type checker uses control flow-based type analysis to dynamically narrow types based on code flow. This enables TypeScript to infer more specific types during runtime, such as narrowing a string variable to a string literal based on conditional checks.


Quiz

  1. Which statement best describes a collective type in TypeScript?
  • a) A type that represents exactly one possible value
  • b) A type that can hold multiple possible values
  • c) A type reserved only for boolean and numeric values
  • d) A type that never changes once defined
  1. Which of the following is an example of a literal type?
  • a) string
  • b) "Hello"
  • c) number
  • d) Array<string>
  1. What is the term for moving from a wider type (like string) to a narrower type (like "Hello") in TypeScript?
  • a) Type conversion
  • b) Type inference
  • c) Type narrowing
  • d) Type resetting
  1. If you change a variable’s type from "Hello" to string, you are performing which action?
  • a) Type checking
  • b) Type extending
  • c) Type widening
  • d) Type mapping
  1. What does control flow based type analysis do in TypeScript?
  • a) It restricts a variable to one specific value across the entire program
  • b) It restricts a variable to one specific value within a branch of the code
  • c) It automatically converts TypeScript code into JavaScript at runtime
  • d) It disables the compiler’s type checking inside loops and conditionals
  1. Which approach would you use to allow a variable to hold only the exact value "Hello" instead of any string?
  • a) Declare it as any and initialize with "Hello"
  • b) Use a string type
  • c) Annotate the variable with the literal type "Hello"
  • d) Cast the variable to string at runtime