Unions and literals | TypeScript Basics pt2

Unions and literals | TypeScript Basics pt2

Introduction

Many times when writing TypeScript and annotating types, we may want a variable to be able to allow more than one type of value. For instance, if we have an age variable, we may want that variable to be able to accept a string value or a number value depending on certain conditions. We can achieve this by use of the concept called unions.

Unions

It involves expanding a value’s allowed type to be two or more possible types.

There is another concept called Narrowing. Narrowing is a TypeScript process that refines a value of multiple types into a single, specific type.
Put together, unions and narrowing are powerful concepts that allow TypeScript to make informed inferences on your code many other mainstream languages cannot.

Union Types

Take this age variable:

let age = Math.random() > 0.5
    ? 29
    : "29";

The variable age is neither of type number only nor of type string only, it is of either type string or type number. This "either-or" type is called a union.

Declaring Union Types

When declaring a union type, typeScript represents union types using the | (pipe) operator between the possible values.

Let's assume you are collecting the details of a user from a form and the age of the user is not a required field. You declare the age and give it an initial value of null. If the user does enter an age, the entered age will be reassigned to the age variable. To annotate the variable age so as to declare it of either type string or number:

let age: null | number = null;

const handleInput(e) => {
    e.preventDefault()

    // ...Other logic
    let formAge = document.getElementById("age");
    // ...More logic

    if (!!formAge) {
        age = formAge;
    }
}

Note: The order of a union type declaration does not matter. You can write boolean | number or number | boolean and TypeScript will treat both the exact same.

Union Properties

If you have a union type in TypeScript, you can only access member properties that are common to all types within the union. TypeScript will generate a type-checking error if you attempt to access a property that is not present in all possible types within the union.

In the following snippet, age is of type number | string. While .toString() exists in both types and is allowed to be used, .toUpperCase() and .toFixed() are not because .toUpperCase() is missing on the number type and .toFixed() is missing on the string type:

let age = Math.random() > 0.5
    ? "30"
    : 30;

age.toString(); // Ok

age.toUpperCase();
//        ~~~~~~~~~~~
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
//   Property 'toUpperCase' does not exist on type 'number'.

age.toFixed();
//        ~~~~~~~
// Error: Property 'toFixed' does not exist on type 'string | number'.
//   Property 'toFixed' does not exist on type 'string'.”

Narrowing

In TypeScript, narrowing happens when the code indicates that a value is more specific than what was originally defined or declared. If TypeScript realizes that a value's type is now narrower than before, you can use the value as if it were of the more specific type. To narrow types, you can use a type guard, which is a logical check that helps TypeScript understand the narrowing from the code.
TypeScript has two commonly used type guards to achieve this.

Assignment Narrowing

In TypeScript, if you assign a value to a variable directly, the variable's type is narrowed to the type of that value. For example, if the variable is declared as a number | string, but you assign the value "30", TypeScript will narrow the variable's type to string because it knows that it must be a string.

let age: number | string;

age = "30";

age.toUpperCase(); // Ok: string

age.toFixed();
//      ~~~~~~~
// Error: Property 'toFixed' does not exist on type 'string'.

Assignment narrowing occurs in TypeScript when a variable is given a specific union type annotation and an initial value. TypeScript recognizes that although the variable may receive a value of any type within the union later, it starts with the type of its initial value.

Conditional Checks

One way to narrow a variable's value in TypeScript is to use an if statement that checks if the variable is equal to a known value. TypeScript recognizes that inside the body of that if statement, the variable must have the same type as the known value.

// Type of artistID: number | string
let artistID = Math.random() > 0.5
    ? "Prophet Bestman"
    : 51;

if (artistID === "Prophet Bestman") {
    // Type of artistID: string
    artistID.toUpperCase(); // Ok
}

// Type of artistID: number | string
artistID.toUpperCase();
//        ~~~~~~~~~~~
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
//   Property 'toUpperCase' does not exist on type 'number'.

In TypeScript, narrowing a variable's type using conditional logic reflects good coding practices in JavaScript. If a variable can have different types, it's generally recommended to check its type to ensure that it matches what is required. TypeScript enforces this approach to ensure that our code is safe and free of errors.

We're almost done, stay with me.

Literal Types

Take this philosopher variable:

const philosopher = "Hypatia";

What type is philosopher?

At first glance, you might say string—and you’d be correct. philosopher is indeed a string.
But! philosopher is not just any old string. It’s specifically the value "Hypatia". Therefore, the philosopher variable’s type is technically the more specific "Hypatia". This is the concept of a literal type, which is the type of a value that's known to be a specific value of a primitive data type, rather than any value of that type. The primitive type string represents all possible strings, while the literal type "Hypatia" represents only one string with that exact value.'

Literal Assignability

We know that primitive types like numbers and strings cannot be assigned to each other. Similarly, different literal types within the same primitive type, such as "Paid" and "Pending", cannot be assigned to each other. For instance, if a variable specifically status is declared to have a literal type of "Pending", it can only be assigned the value "Pending", but not "Paid" or any other string.

let status: "Pending";

status = "Pending"; // ok

status = "Paid"; 
// Error: Type '"Paid"' is not assignable to type '"Pending"'.”

We can specify the various literals that we want a variable to accept by using union types.

let status: "Pending" | "Unpaid" | "Paid" ;

status = "Pending"; // ok
status = "Paid"; // ok
status = "Unpaid"; // ok