Taming TypeScript

The TypeScript Tutorial for The Hater

Unions and the Power of Literals

What if a value can be more than one type?

So far, we've dealt with values that are one specific type. But what if a variable could be either a string OR a number? This is a common scenario, and TypeScript handles it with union types.

The | Operator

You create a union type by joining two or more types with the pipe | character.

let myId: string | number;

Now, myId can be assigned either a string or a number. When you have a union, you can only perform operations that are valid for every type in that union. To do type-specific operations, you first need to check what the type is. This is called narrowing.

Exercise 1: Narrowing with typeof

  1. The processId function takes an id of type string | number.
  2. It needs to return the uppercased string if it's a string, or the number formatted to two decimal places if it's a number.
  3. Use an if statement with a typeof check to narrow the type and implement the logic.
Interactive Editor
Loading...
Click to see the solution
function processId(id: string | number) {
  if (typeof id === "string") {
    // Inside this block, TypeScript knows 'id' is a string
    return id.toUpperCase();
  } else {
    // Here, it knows 'id' is a number
    return id.toFixed(2);
  }
}

Combining Unions with Literal Types

The typeof guard is great for primitives. But the true power of unions is unlocked when you combine them with literal types.

Remember in Lesson 1, we saw that a const variable gets its value as its type? const charName = "Bowser"; // Type is "Bowser"

We can use this concept to build unions of specific values. This allows us to define a finite set of allowed options for a variable, which is an incredibly powerful pattern.

type Status = 'pending' | 'success' | 'error';

A variable of type Status can only ever be one of those three exact strings. This makes typos and invalid states impossible.

Exercise 2: The Status Checker

  1. Define a type alias named LogLevel that can only be 'info', 'warn', or 'error'.
  2. The log function takes a message and a level of type LogLevel.
  3. The last function call has a typo ('errro'). Because we're using a literal union, TypeScript catches this bug. Fix the typo.
  4. Inside the function, use a switch statement to handle the different log levels.
Interactive Editor
Loading...
Click to see the solution
type LogLevel = 'info' | 'warn' | 'error';

function log(message: string, level: LogLevel) {
  switch (level) {
    case 'info':
      console.log(`INFO: ${message}`);
      break;
    case 'warn':
      console.warn(`WARN: ${message}`);
      break;
    case 'error':
      console.error(`ERROR: ${message}`);
      break;
  }
}

log("User logged in.", "info");
log("User password expired.", "warn");
log("DB connection failed.", "error");

By combining union and literal types, you create a powerful safety net. You are explicitly telling TypeScript the exact values that are valid, making your code more robust, easier to read, and eliminating an entire category of bugs.