Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
TypeScriptJavaScriptJune 20, 20264 min read

TypeScript narrowing: How to make the compiler trust your code

TypeScript narrowing is the key to writing type-safe code without constant casting. Learn how to guide the compiler through your logic for cleaner builds.

TypeScriptJavaScriptProgrammingWeb DevelopmentSoftware EngineeringFrontend
Detailed view of code and file structure in a software development environment.

I remember the first time I fought the TypeScript compiler for an entire afternoon. I had a function that accepted a user ID—either a string or a number—and I was trying to perform a toLowerCase() on it. The compiler kept throwing errors, telling me that number doesn't have a toLowerCase method. I kept resorting to as string just to get the build to pass.

That was my first lesson in TypeScript narrowing. If you’re tired of fighting the compiler with "any" or unsafe type assertions, you need to learn how to prove your logic to the type checker. Instead of forcing your will on TypeScript, you teach it to follow your path.

Why narrowing matters

When you define a variable as string | number, TypeScript stays neutral. It doesn't know which one you have, so it restricts you to the intersection of the two types—which is almost nothing. Narrowing is the process of checking a value at runtime and letting the compiler know that within a specific block, the type is now more specific.

It’s the difference between guessing and knowing. When you narrow correctly, you stop writing defensive code that checks for properties that are already guaranteed to exist.

The basic tools of narrowing

You’re likely already using narrowing without calling it by name. The most common patterns are built into the language:

  1. Type Guards (typeof): The easiest way to handle primitives.
  2. Truthiness Checks: Checking if an object is null or undefined.
  3. Equality Checks: Using === or !== to isolate specific values.
  4. The in Operator: Perfect for checking if a property exists on an object.

Let's look at a concrete example. Suppose you have a configuration object that might be a local path or a remote URL.

TYPESCRIPT
type Config = { path: string } | { url: string };

function connect(config: Config) {
  if (CE9178">'url' in config) {
    // TypeScript knows this is the second type
    console.log(CE9178">`Connecting to ${config.url}`);
  } else {
    // TypeScript knows this is the first type
    console.log(CE9178">`Loading from ${config.path}`);
  }
}

By using the in operator, I didn't have to cast anything. The compiler saw the branch and narrowed the type automatically. This approach is much safer than using type guards that might get out of sync with your data structures, much like how we use discriminated unions in TypeScript: modeling state without bugs to keep complex states predictable.

When narrowing gets tricky

Scattered wooden alphabet letters with the word 'WHEN' on a black surface, creative concept.

Sometimes, simple guards aren't enough. I once had a project using TypeScript 4.7 where I was filtering an array of mixed objects. I tried to filter out null values, but the array type remained (T | null)[].

I initially tried to just use .filter(Boolean), but that doesn't tell TypeScript that the nulls are gone. The fix is a custom type predicate:

TYPESCRIPT
function isDefined<T>(item: T | null | undefined): item is T {
  return item !== null && item !== undefined;
}

const data = [1, 2, null, 4].filter(isDefined);
// Now data is inferred as number[]

The item is T syntax is the secret sauce. It tells the compiler: "If this function returns true, treat the input as type T." It’s an explicit contract you sign with the compiler.

Common pitfalls to avoid

Don't overcomplicate your guards. I’ve seen teams write massive, nested if statements just to satisfy the compiler. If you find yourself needing three levels of narrowing, your data structure is probably the problem, not your type definitions.

Also, be careful with instanceof. It works great for classes, but it fails across different execution contexts (like iframes or certain bundler setups). I spent about two days debugging a production issue where instanceof failed because the objects were being passed between different window contexts. Stick to property checks like in or literal checks when you can.

Practical application: Bringing it together

Hands squeezing colorful paint onto a sheet in a vibrant art class setting.

When you're building a system that requires high reliability, narrowing is your best friend. Whether you are implementing idempotency keys: making retries safe in distributed systems or handling API responses, you want to be certain about your data shape.

Narrowing allows you to write code that is both expressive and incredibly strict. It transforms the compiler from an annoying gatekeeper into a partner that helps you catch null pointers before they hit your users.

Next time you’re tempted to use as any, stop. Ask yourself: "How can I prove this to TypeScript?" Usually, a simple typeof or a custom predicate is all you need. I'm still learning new ways to handle complex generics, but mastering these basic narrowing patterns has saved me from more bugs than I can count.

Back to Blog

Similar Posts

A vibrant workspace showing computer monitors with code, keyboard, and tech accessories.
TypeScriptJavaScriptJune 20, 20264 min read

Generics in TypeScript that actually pay off for your codebase

Generics in TypeScript can feel like an academic hurdle, but they pay off when you use them to enforce type safety in API calls and reusable components.

Read more
Typewritten note with 'I love you' on vintage paper background, evoking nostalgia.
TypeScriptJavaScriptJune 20, 20264 min read

TypeScript utility types you will reach for weekly

TypeScript utility types can save you hours of boilerplate code. Learn the essential tools I use weekly to keep my production projects clean and type-safe.

Read more
Detailed view of programming code in a dark theme on a computer screen.
TypeScriptJavaScriptJune 20, 20264 min read

Typing async code in TypeScript without fighting the compiler

Typing async code in TypeScript doesn't have to be a battle. Learn how to handle promises, API responses, and error states without fighting the compiler.

Read more