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.
function showLevel(
level: number | string
): void {
if (typeof level === "number") {
console.log(level + 1);
} else {
console.log(level.toUpperCase());
}
}
showLevel(10);
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.
let developerId: string | number;
developerId = 108;
console.log(developerId);
developerId = "MC-108";
console.log(developerId);Both assignments are valid because developerId accepts either a string or a number. A boolean, however, would not be accepted.
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.
function findDeveloper(id: string | number): void {
console.log("Searching for: " + id);
}
findDeveloper(108);
findDeveloper("Moe");The function accepts both supported forms without using any.
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.
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.
Recognize Strings and Numbers
The JavaScript typeof operator is one of the simplest ways to narrow primitive types.
function formatValue(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
formatValue("typescript");
formatValue(10);Inside the first branch, TypeScript knows value is a string. Inside the else branch, it knows the value must be a number.
Let TypeScript Follow Your Logic
TypeScript analyzes conditional statements and uses them to narrow types as your program runs through different branches.
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");Because the number branch returns early, TypeScript knows the remaining code is working with a string.
Identify Class Instances
When working with classes, instanceof can check whether an object was created from a particular constructor.
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"));This works because both classes exist as JavaScript constructors at runtime, unlike interfaces, which are removed during compilation.
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.
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" });Checking for a distinctive property helps determine which object structure you’re working with.
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.
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" });TypeScript uses kind to determine which properties are available in each branch.
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.
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);The expression value is string is a type predicate. Your function must actually perform a reliable check; TypeScript trusts the predicate you declare.
unknown over any when accepting data of an uncertain type. It requires you to check a value before using type-specific operations.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.
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");One function now handles two different kinds of developer progress while keeping both branches type-safe.
Create a Developer Status Checker
Build a function that understands both numeric developer levels and descriptive ranks.
Your Mission
- Create a type alias named
DeveloperLevelthat acceptsnumber | string. - Create a function named
showDeveloper()that accepts a name and a developer level. - Print the developer’s name.
- Use
typeofto determine whether the level is a number or string. - For numbers, print the current level and the next level.
- For strings, print the rank in uppercase.
- Test your function with
10and"expert".
boolean to your union and handle it with a third branch that displays whether the developer is active.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.
Preparing TypeScript sandbox…
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 CompletedYou’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!
