Back to Blog
Lesson 15 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 2, 20264 min read

Type Narrowing with Conditionals in TypeScript

Master Type Narrowing with Conditionals to write safer, more predictable code. Learn how TypeScript's control flow analysis refines your types automatically.

TypeScriptType NarrowingControl FlowConditionalsSafetyProgramming Best Practices
A vintage typewriter with a paper displaying 'Terms of Service'. Perfect for business or legal themes.

Previously in this course, we explored Union Types for Flexibility in TypeScript and how to constrain them using Literal Types for Status Codes: Enforcing Constraints in TypeScript. While unions provide the flexibility to handle multiple data shapes, they introduce a challenge: how do we tell the compiler which specific type we are dealing with at any given moment?

This is where Type Narrowing comes in. By using standard JavaScript control flow—like if statements—TypeScript analyzes your code and automatically "narrows" a broad union type down to a specific, usable type.

Understanding Control Flow Analysis

TypeScript is smart enough to trace the path your code takes. When you perform a check inside a conditional block, the compiler realizes that the variable inside that block can only be a specific member of the union.

Think of it as a funnel. You start with a wide "Union" type (e.g., string | number), and as you add conditional logic, you shrink that type until you have exactly what you need.

A Concrete Example: Processing Task IDs

In our ongoing Task API client project, a task ID might come in as either a string (from a URL parameter) or a number (from our database). We need to handle both gracefully.

TYPESCRIPT
type TaskId = string | number;

function processTaskId(id: TaskId) {
  // At this point, CE9178">'id' is still string | number
  
  if (typeof id === CE9178">'string') {
    // TypeScript knows CE9178">'id' is a string here!
    console.log(CE9178">`Processing string ID: ${id.toUpperCase()}`);
  } else {
    // TypeScript knows CE9178">'id' must be a number here
    console.log(CE9178">`Processing numeric ID: ${id.toFixed(0)}`);
  }
}

In the example above, the typeof check serves as a type guard. Without that if block, calling .toUpperCase() on a number would cause a compilation error. By adding the conditional, we provided the compiler with the proof it needed to allow the operation.

Why Safety Matters

A worker focusing on safety outdoors, wearing a red helmet with a safety logo.

Type Narrowing isn't just about satisfying the compiler; it's about runtime safety. If you accidentally attempt to perform an operation that doesn't exist on one of the union types, TypeScript will stop you before you even run your code.

ApproachType SafetyRuntime Risk
Ignoring typesNoneHigh (runtime crashes)
Casting (as string)Low (manual, error-prone)Medium
Type NarrowingHigh (verified by compiler)Minimal

Hands-on Exercise

Let's apply this to our Task API client. Imagine a function that handles an API response that can either be a Task object or a string error message.

  1. Create a Task interface with an id and title.
  2. Define a type alias ApiResponse as Task | string.
  3. Write a function displayResponse(response: ApiResponse) that checks if the response is a string. If it is, log the error; otherwise, log the task title.

Hint: Remember that typeof works on primitives, but you can also check for properties if the union contains objects!

Common Pitfalls

Even experienced developers run into these issues when narrowing types:

  • Forgetting the else branch: If you check for one type in an if block, TypeScript assumes the "remaining" types exist in the else block. If you aren't exhaustive, you might miss a potential undefined or null case.
  • Over-complicating checks: You don't need complex logic for simple types. Stick to typeof for primitives.
  • Assuming properties exist: Accessing a property like response.title before checking if response is an object will result in a compiler error. Always narrow the type before accessing properties specific to that type.

FAQ

Can I use switch statements for narrowing? Yes! switch statements are excellent for narrowing, especially when working with union types that rely on a common property (like a status string).

What if I have a union of three or more types? You can chain if/else if statements. TypeScript will continue to narrow the type with each condition until the final else block, which will contain whatever is left of the original union.

Does this work with null or undefined? Absolutely. Checking if (value !== null) is a standard way to narrow a type from string | null to string.

Recap

Team members presenting a project in a modern office setting with a focus on collaboration.

Type Narrowing with conditionals turns broad, ambiguous types into precise, safe ones. By using standard JavaScript control flow, we allow TypeScript's engine to track our logic and guarantee that we only call methods or access properties that actually exist on the current object. This leads to cleaner code and fewer runtime bugs in our API client.

Up next: We will dive deeper into the typeof operator and learn how to write more sophisticated type guards.

Similar Posts