Skip to main content
Go back

TypeScript #6: Type Guards & Narrowing

#typescript #narrowing #basics

Learn to tell TypeScript "trust me, I know what I'm doing". Type Guards, 'is' operator and narrowing.

TypeScript is very smart, but sometimes it needs “clues” to know exactly what type a variable is at a given moment. This process of refining a broad type (like string | number) to a more specific one (like string) is called Narrowing.

And to achieve this, we use Type Guards.

1. The problem: Uncertainty

Imagine you have a function that accepts an ID, but this ID can be a number (SQL Database) or a text (UUID).

typescript
function printId(id: string | number) {
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
console.log(id.toUpperCase()); 
}

printId("abc");

TypeScript complains because it is not sure that id is a string. If it were a number, .toUpperCase() would crash at runtime. 💥

2. Solution: typeof Guards

The most common form of narrowing is using the JavaScript typeof operator inside an if conditional. TypeScript understands this code and “reduces” the type inside the block.

typescript
function printId(id: string | number) {
if (typeof id === "string") {
  // Inside here TypeScript KNOWS that id is string!
  console.log("Your ID is: " + id.toUpperCase());
} else {
  // And here it knows it HAS to be number
  console.log("Your ID is: " + id.toFixed(2));
}
}

printId("Pizza!");
printId(123.456);
Types supported by typeof

Remember that typeof only works with basic primitives: "string", "number", "boolean", "symbol", "undefined", "object", and "function". Watch out! typeof null returns "object", which is a historical JS bug.

3. Truthiness Narrowing

Sometimes you don’t need to know the exact type, only if the value exists (is not null or undefined).

typescript
function printName(name?: string) {
// name is string | undefined
if (name) {
  // Here name is string (because undefined is falsy)
  console.log(name.toUpperCase());
}
}

4. instanceof Narrowing

For objects built with classes, typeof is not very useful (everything is “object”). Here instanceof comes in using new.

typescript
function logValue(x: Date | string) {
if (x instanceof Date) {
  console.log(x.toUTCString()); // x is Date
} else {
  console.log(x.toUpperCase()); // x is string
}
}

5. Type Predicates (is)

This is the most “Pro” part. What if we have a custom verification logic? We can create a function that returns a Type Predicate: param is Type.

typescript
interface Fish { swim: () => void }
interface Bird { fly: () => void }

// Narrowing Function
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

// Usage
function move(pet: Fish | Bird) {
if (isFish(pet)) {
  pet.swim(); // TS knows it is Fish
} else {
  pet.fly(); // TS knows it HAS to be Bird
}
}

6. Exercise: The Universal Formatter

You have a function that receives safe inputs, but you don’t know if they will come as string or number. Your job is to format them correctly without TypeScript complaining.

Ideally make use of typeof