Taming TypeScript

The TypeScript Tutorial for The Hater

The `is` Keyword: A Safe Investigation

The Problem: The Limits of as

In the last lesson, we learned about the as keyword. It's a powerful command, but it's a gamble. When we're dealing with data from an external API, we can't just assume its shape. We need to prove it.

How can we run an investigation and, if it passes, tell TypeScript that our data is now safe to use?

The Solution: User-Defined Type Guards with is

We can create a special function called a user-defined type guard. This function performs whatever checks we want. Its magic lies in its return signature, which uses the is keyword. This is called a type predicate.

as is a COMMAND. is is a CONCLUSION.

  • value as User: You force the type. You take the risk.
  • isUser(value): You run an investigation. If the function returns true, TypeScript will treat the value as a User from that point onward.

Let's build a type guard to validate our user data.

Step 1: The Function Signature

First, we define our User type and the signature of our type guard function. Notice the return type data is User.

type User = {
  id: number;
  name: string;
  email: string;
};

function isUser(data: unknown): data is User {
  // Investigation logic will go here
}

That data is User signature is a promise we're making to TypeScript: "If this function returns true, I guarantee that the data object you passed in is a valid User."

Step 2: Building the Investigation

Now, let's write the logic. A robust check involves multiple steps.

Check 1: Is it a non-null object?

The most basic check. A User must be an object.

if (typeof data !== 'object' || data === null) {
  return false;
}

Check 2: Does it have the right properties?

Now that we know we have an object, we can use the in operator to check if it has the properties we expect. For the purpose of this lesson, we will only check for the presence of the keys. A more advanced guard would also verify that the values have the correct types, not just the right keys. We will learn how to build these more powerful guards when we cover Generics in a future lesson.

The core principle is the in check.

Step 3: The Complete Type Guard

Our complete type guard will first check if the data is an object, and then use the in operator to ensure all the required keys exist.

When we combine these checks for all our properties, we get a complete, robust type guard. We can make it more readable by assigning each check to a variable.

Interactive Editor
Loading...
💡 Hint: How would you start checking value types?

As our warning mentioned, a truly robust guard also checks the types of the values. How would you start doing that?

You can use the same two-part handshake pattern we learned earlier. Here's how you could check just the id property's type:

function isUserWithIdTypeCheck(data: unknown): data is User {
  if (typeof data !== 'object' || data === null) {
    return false;
  }

  // This is the key!
  const hasTypedId = 'id' in data && typeof (data as { id: unknown }).id === 'number';

  const hasName = 'name' in data;
  const hasEmail = 'email' in data;

  return hasTypedId && hasName && hasEmail;
}

const userWithWrongType = { id: "123", name: "Kevin", email: "[email protected]" };
console.log("--- Checking with Type Check ---");
console.log("Checking userWithWrongType:", isUserWithIdTypeCheck(userWithWrongType)); // false!

This is the foundation we will build upon in the Generics module to create fully robust, reusable type guards.

💡 Hint: How would you start checking value types?

As our warning mentioned, a truly robust guard also checks the types of the values. How would you start doing that?

You can use the same two-part handshake pattern we learned earlier. Here's how you could check just the id property's type:

function isUserWithIdTypeCheck(data: unknown): data is User {
  if (typeof data !== 'object' || data === null) {
    return false;
  }

  // This is the key!
  const hasTypedId = 'id' in data && typeof (data as { id: unknown }).id === 'number';

  const hasName = 'name' in data;
  const hasEmail = 'email' in data;

  return hasTypedId && hasName && hasEmail;
}

const userWithWrongType = { id: "123", name: "Kevin", email: "[email protected]" };
console.log("--- Checking with Type Check ---");
console.log("Checking userWithWrongType:", isUserWithIdTypeCheck(userWithWrongType)); // false!

This is the foundation we will build upon in the Generics module to create fully robust, reusable type guards.

Conclusion: as vs. is - A Clear Winner

You have now learned the difference between the two ways of narrowing unknown types.

  • as: A risky command. Use it sparingly.
  • is: A safe investigation. This is the professional's choice for validating external data.

In the final lesson of this module, we will put our new isUser type guard to use in a real fetch request.