WHAT'S NEW
TypeScript · Lesson 10 of 11

Union Types & Type Guards

Handle multiple possible types confidently. Learn how to check values, narrow types, and build functions that respond safely to different kinds of data.

Union TypestypeofType NarrowingDiscriminated Unions
● ● ●   type-guards.ts
function showLevel(
  level: number | string
): void {
  if (typeof level === "number") {
    console.log(level + 1);
  } else {
    console.log(level.toUpperCase());
  }
}

showLevel(10);
CONSOLE OUTPUT
> Ready to run…
01 / Understanding Union Types

One Value, Multiple Possible Types

Until now, you’ve mostly assigned one type to each variable. But real applications often need to handle values that can arrive in different forms.

A union type lets a value belong to any one of several specified types. Use the vertical bar | to separate the possibilities.

union-types.ts
let developerId: string | number;

developerId = 108;
console.log(developerId);

developerId = "MC-108";
console.log(developerId);
Console Output:
108 MC-108

Both assignments are valid because developerId accepts either a string or a number. A boolean, however, would not be accepted.

Remember: A union describes alternative types, not a requirement that a value be all those types at once.
02 / Why Union Types Matter

Handle Real-World Data More Flexibly

Imagine a website that identifies developers by a numeric ID or a username. Instead of writing separate functions, you can use one function with a union parameter.

flexible-parameters.ts
function findDeveloper(id: string | number): void {
  console.log("Searching for: " + id);
}

findDeveloper(108);
findDeveloper("Moe");
Console Output:
Searching for: 108 Searching for: Moe

The function accepts both supported forms without using any.

03 / The Need for Type Guards

Check a Type Before Using Its Features

When a variable can contain different types, TypeScript won’t automatically let you use a method that only exists on one of them.

unsafe-union.ts
function formatValue(value: string | number): void {
  // TypeScript error:
  // console.log(value.toUpperCase());
}

The problem is that toUpperCase() is a string method. A number doesn’t have it.

A type guard is a check that helps TypeScript narrow a value to a more specific type before you use it.

04 / Using typeof

Recognize Strings and Numbers

The JavaScript typeof operator is one of the simplest ways to narrow primitive types.

typeof-guard.ts
function formatValue(value: string | number): void {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

formatValue("typescript");
formatValue(10);
Console Output:
TYPESCRIPT 10.00

Inside the first branch, TypeScript knows value is a string. Inside the else branch, it knows the value must be a number.

05 / Type Narrowing

Let TypeScript Follow Your Logic

TypeScript analyzes conditional statements and uses them to narrow types as your program runs through different branches.

narrowing.ts
function describeLevel(level: string | number): void {
  if (typeof level === "number") {
    console.log("Numeric level: " + level);
    return;
  }

  console.log("Rank: " + level.toUpperCase());
}

describeLevel(10);
describeLevel("expert");
Console Output:
Numeric level: 10 Rank: EXPERT

Because the number branch returns early, TypeScript knows the remaining code is working with a string.

Good habit: Use early returns when they make type checks and function logic easier to read.
06 / Using instanceof

Identify Class Instances

When working with classes, instanceof can check whether an object was created from a particular constructor.

instanceof.ts
class Developer {
  constructor(public name: string) {}
}

class Course {
  constructor(public title: string) {}
}

function describe(item: Developer | Course): void {
  if (item instanceof Developer) {
    console.log("Developer: " + item.name);
  } else {
    console.log("Course: " + item.title);
  }
}

describe(new Developer("Moe"));
describe(new Course("TypeScript"));
Console Output:
Developer: Moe Course: TypeScript

This works because both classes exist as JavaScript constructors at runtime, unlike interfaces, which are removed during compilation.

07 / The in Operator

Check Whether a Property Exists

The in operator checks whether a property exists on an object or its prototype chain. It can also help TypeScript narrow unions of object shapes.

in-operator.ts
interface Developer {
  name: string;
}

interface Course {
  title: string;
}

function describe(item: Developer | Course): void {
  if ("name" in item) {
    console.log("Developer: " + item.name);
  } else {
    console.log("Course: " + item.title);
  }
}

describe({ name: "Moe" });
describe({ title: "TypeScript" });
Console Output:
Developer: Moe Course: TypeScript

Checking for a distinctive property helps determine which object structure you’re working with.

08 / Discriminated Unions

Give Each Object Type a Clear Identifier

For more complex applications, a discriminated union uses a shared property with distinct literal values to identify each possible object shape.

discriminated-union.ts
type LearningStatus =
  | { kind: "completed"; score: number }
  | { kind: "in-progress"; lesson: string };

function showStatus(status: LearningStatus): void {
  if (status.kind === "completed") {
    console.log("Score: " + status.score);
  } else {
    console.log("Learning: " + status.lesson);
  }
}

showStatus({ kind: "completed", score: 100 });
showStatus({ kind: "in-progress", lesson: "Generics" });
Console Output:
Score: 100 Learning: Generics

TypeScript uses kind to determine which properties are available in each branch.

09 / Custom Type Guards

Create Reusable Type Checks

You can write your own type guard function using a type predicate. This tells TypeScript what type a value has when the function returns true.

custom-guard.ts
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function display(value: unknown): void {
  if (isString(value)) {
    console.log(value.toUpperCase());
  } else {
    console.log("Not a string");
  }
}

display("Moe");
display(10);
Console Output:
MOE Not a string

The expression value is string is a type predicate. Your function must actually perform a reliable check; TypeScript trusts the predicate you declare.

Important: Prefer unknown over any when accepting data of an uncertain type. It requires you to check a value before using type-specific operations.
10 / Putting It Together

Build a Flexible Developer Status Checker

Let’s combine union types, typeof, and narrowing in a program that accepts either a numeric level or a descriptive rank.

developer-status.ts
type DeveloperLevel = number | string;

function showDeveloper(
  name: string,
  level: DeveloperLevel
): void {
  console.log("Developer: " + name);

  if (typeof level === "number") {
    console.log("Level: " + level);
    console.log("Next level: " + (level + 1));
  } else {
    console.log("Rank: " + level.toUpperCase());
  }
}

showDeveloper("Moe", 10);
showDeveloper("Moe", "expert");
Console Output:
Developer: Moe Level: 10 Next level: 11 Developer: Moe Rank: EXPERT

One function now handles two different kinds of developer progress while keeping both branches type-safe.

🚀 mC0de Challenge

Create a Developer Status Checker

Build a function that understands both numeric developer levels and descriptive ranks.

Your Mission

  1. Create a type alias named DeveloperLevel that accepts number | string.
  2. Create a function named showDeveloper() that accepts a name and a developer level.
  3. Print the developer’s name.
  4. Use typeof to determine whether the level is a number or string.
  5. For numbers, print the current level and the next level.
  6. For strings, print the rank in uppercase.
  7. Test your function with 10 and "expert".
Extra challenge: Add boolean to your union and handle it with a third branch that displays whether the developer is active.
💻 Interactive TypeScript Sandbox

Build Your Developer Status Checker!

Change the developer level between a number and a string, then run your code. TypeScript checks your types before the compiled JavaScript executes.

Console Output
Lesson 10 Recap

What You’ve Learned

You’re Handling Complex Types with Confidence!

You can now:

  • Create union types using |.
  • Accept multiple types without relying on any.
  • Narrow primitive types using typeof.
  • Understand control-flow-based type narrowing.
  • Check class instances with instanceof.
  • Check object properties using in.
  • Build discriminated unions using literal properties.
  • Create custom type guards with type predicates.

Next up: TypeScript Final Project — bring everything together in a practical program!

Your Learning Journey

0 / 11 Completed

You’re one lesson away from completing the TypeScript pathway!

🎉

You’re Almost at the Finish Line!

You’ve learned how to handle union types, narrow values safely, and create type guards that make your code more reliable. Just one final lesson remains in your TypeScript journey!

PIX0
</> {} JS 01
0101 </> CSS JS HTML PIX0 const {} 1010 async mC0de function return 0101 <div> 1010 PIX0 CSS JS HTML const {} 0101 mC0de
PIX0 // WORLD OF CODE
PIX0 IS PREPARING SOMETHING AWESOME FOR YOU 0%