The is Keyword in TypeScript: Type Predicates Explained

The is keyword in TypeScript turns a function into a custom type guard. Write value is Type as the return type, and when the function returns true, TypeScript treats the argument as that type inside the if block:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

Nothing here is hand-typed output: I compiled the snippets under TypeScript 7.0.2 and Node.js v22.22.2 with --strict and copied each console result.

You’ll see unions, unknown API data, filter(), a guard that lies, and asserts.

What does the is keyword do in TypeScript?

is only works in a function’s return type, as parameterName is Type. That’s a type predicate: the body returns a boolean, and the predicate says what true means.

Here a payment can be a card or PayPal. isCard() narrows both branches:

interface CardPayment { kind: "card"; last4: string; amount: number }
interface PayPalPayment { kind: "paypal"; email: string; amount: number }
type Payment = CardPayment | PayPalPayment;

function isCard(p: Payment): p is CardPayment {
  return p.kind === "card";
}

const payments: Payment[] = [
  { kind: "card", last4: "4242", amount: 89.99 },
  { kind: "paypal", email: "mia@example.com", amount: 45.5 },
];

for (const p of payments) {
  if (isCard(p)) {
    console.log(`Card ending ${p.last4}: $${p.amount.toFixed(2)}`);   // p is CardPayment
  } else {
    console.log(`PayPal ${p.email}: $${p.amount.toFixed(2)}`);       // p is PayPalPayment
  }
}

Output:

Card ending 4242: $89.99
PayPal mia@example.com: $45.50
Command Prompt output of a TypeScript is keyword type guard narrowing a card or PayPal payment union
Inside the if, p is CardPayment; in the else, TypeScript knows it’s PayPalPayment.

An inline p.kind === "card" narrows too; a guard pays off when you reuse the check. My TypeScript type narrowing guide covers the built-in checks.

Why use a type predicate instead of a boolean return?

A boolean return type tells the compiler nothing about the argument. Compare:

function isStringBool(value: unknown): boolean {
  return typeof value === "string";
}

function isString(value: unknown): value is string {
  return typeof value === "string";
}

const input: unknown = "Dallas";

if (isString(input)) {
  console.log(input.toUpperCase());     // OK: narrowed to string
}
if (isStringBool(input)) {
  console.log(input.toUpperCase());     // error: still unknown
}

Output:

is_boolean.ts(15,15): error TS18046: 'input' is of type 'unknown'.
Command Prompt showing tsc error TS18046 when a boolean-returning check doesn't narrow an unknown value, while the is version compiles
The is string guard compiles; the boolean one leaves input as unknown.

Same runtime check, different return type. That’s why is matters most with unknown, which you can’t touch until it’s narrowed, unlike the any type in TypeScript.

Use a type guard to check unknown API data

JSON from a fetch call has no real type at runtime. A guard that checks each property lets you trust it, and filter() takes it directly:

interface Order { id: number; city: string; total: number }

function isOrder(x: unknown): x is Order {
  return (
    typeof x === "object" && x !== null &&
    typeof (x as Order).id === "number" &&
    typeof (x as Order).city === "string" &&
    typeof (x as Order).total === "number"
  );
}

const body: unknown = JSON.parse(`[
  {"id": 1, "city": "Seattle", "total": 120.4},
  {"id": 2, "city": "Boston"},
  {"id": 3, "city": "Phoenix", "total": 75}
]`);

if (Array.isArray(body)) {
  const orders: Order[] = body.filter(isOrder);
  console.log("valid orders:", orders.length, "of", body.length);
  for (const o of orders) console.log(`#${o.id} ${o.city.padEnd(8)} $${o.total.toFixed(2)}`);
}

const cities = ["Miami", null, "Denver", undefined];
const clean = cities.filter((c) => c != null);    // TS 5.5+ infers c is string
console.log(clean.map((c) => c.toUpperCase()));

Output:

valid orders: 2 of 3
#1 Seattle  $120.40
#3 Phoenix  $75.00
[ 'MIAMI', 'DENVER' ]
Command Prompt output of a TypeScript is keyword guard filtering unknown JSON into an Order array and removing null values
The order missing total is dropped, and filter(isOrder) returns Order[], not unknown[].

Since TypeScript 5.5, c => c != null gets an inferred predicate, so clean is string[], a quick way of removing undefined values from an array. More interface checks are in how to check if an object matches an interface.

Can a type guard be wrong?

Yes. TypeScript trusts the predicate and never verifies that the body proves it. This guard checks only name but promises a full Customer:

interface Customer { name: string; address: { state: string } }

// Wrong check: only looks at name, but promises a full Customer
function isCustomer(x: unknown): x is Customer {
  return typeof x === "object" && x !== null && "name" in x;
}

const row: unknown = { name: "Ava Reed" };    // no address

if (isCustomer(row)) {
  try {
    console.log(row.address.state);           // compiles fine
  } catch (err) {
    console.log("Runtime:", (err as Error).name, "-", (err as Error).message);
  }
}

Output:

Runtime: TypeError - Cannot read properties of undefined (reading 'state')
Command Prompt showing a TypeScript type guard that compiles but throws a runtime TypeError because the check was incomplete
No compile error, but reading address.state crashes because the guard lied.

So check every property the code after the guard relies on. It’s the same risk as a careless type assertion in TypeScript, just hidden inside a function.

is vs asserts: assertion functions

An asserts value is Type function narrows everything after the call, and throws when the check fails:

function assertIsNumber(value: unknown, label: string): asserts value is number {
  if (typeof value !== "number" || Number.isNaN(value)) {
    throw new TypeError(`${label} must be a number, got ${JSON.stringify(value)}`);
  }
}

function applyTax(price: unknown): string {
  assertIsNumber(price, "price");
  return (price * 1.0825).toFixed(2);          // price is number from here on
}

console.log("Austin total: $" + applyTax(49.99));
try {
  applyTax("49.99");
} catch (err) {
  console.log((err as Error).message);
}

Output:

Austin total: $54.11
price must be a number, got "49.99"

Reach for asserts when your input must be valid, and is when you handle both outcomes.

Type predicate syntax at a glance

FormNarrowsUse it for
x is TThe if and else branchesReusable checks, filter()
asserts x is TEverything after the callValidating input, throwing on bad data
this is TThe object the method is called onClass methods like isFile()
booleanNothingChecks where the type doesn’t matter

Once guards feel natural, these type-checking articles pair well with them:

Frequently asked questions

What is the is keyword in TypeScript?

It creates a type predicate. A function returning value is Type tells TypeScript that a true result means the argument is that type.

Can I use is outside a function return type?

No. is is only valid as a return type, such as x is string or asserts x is string.

Does TypeScript check that my type guard is correct?

No. It trusts the predicate. A guard that checks too little compiles fine and fails at runtime, so test every property you rely on.

What’s the difference between is and as?

as forces a type with no runtime check. An is guard runs real code first and narrows only when it returns true.

Do I still need is for filter() in TypeScript 5.5 and later?

Often not for simple checks like x != null, which get an inferred predicate. Complex object checks still need an explicit is guard.

Where is the is keyword documented?

See Using type predicates in the TypeScript Handbook.