DEV Community

loading...

Aha! Understanding Typescript's Type Predicates

daveturissini profile image David ・2 min read

This article will cover Type Predicates at a high level. To understand Type Predicates, we must first understand how they are used in relation to union types.

Union Types

In Typescript, a variable isn't always restricted to a single type. Union Types are a way to declare multiple types onto a single value.

// value can now be set to a `string`, `boolean`, or `null` value. 
let value: string | boolean | null = ...

interface Cat {
  numberOfLives: number;
}
interface Dog {
  isAGoodBoy: boolean;
}

let animal: Cat | Dog = ...

When we use union types, we have to do work to narrow the possible types down to the current value's actual type. Type Guards are what allow us to do this narrowing.

Type Guards

According to the official docs,

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

Put another way, type guards guarantee that a string is a string when it could also be a number.

Type guards are not entirely different than doing feature detection. The big idea is to try to detect properties, methods or prototypes to figure out how to handle a value. There are four main ways to use type guards:

  • in keyword
  • typeof keyword
  • instanceof keyword
  • type predicates with custom type guard

Type Predicate

While you are probably familiar with "in", "typeof", and "instanceof", you might be wondering what "type predicates" are. Type predicates are a special return type that signals to the Typescript compiler what type a particular value is. Type predicates are always attached to a function that takes a single argument and returns a boolean. Type predicates are expressed as argumentName is Type.

interface Cat {
  numberOfLives: number;
}
interface Dog {
  isAGoodBoy: boolean;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return typeof animal.numberOfLives === 'number';
}

For sample function, isCat, is executed at run time just like all other type guards. Since this function returns a boolean and includes the type predicate animal is Cat, the Typescript compiler will correctly cast the animal as Cat if isCat evaluates as true. It will also cast animal as Dog if isCat evaluates as false.


let animal: Cat | Dog = ...

if (isCat(animal)) {
  // animal successfully cast as a Cat
} else {
  // animal successfully cast as a Dog
}

Pretty neat! Perhaps the best thing about custom type guards and type predicates is not only we can use in, instanceof, and typeof in our type guards but we can also custom type checks. As long as our function returns a boolean, Typescript will do the right thing.

Discussion (4)

pic
Editor guide
Collapse
karataev profile image
Eugene Karataev

Thanks for the type predicate explanation. But in my case TS (v.3.8.3) is not happy with isCat function:

function isCat(animal: Cat | Dog): animal is Cat {
  return typeof animal.numberOfLives === 'number';
// Property 'numberOfLives' does not exist on type 'Cat | Dog'.
//  Property 'numberOfLives' does not exist on type 'Dog'.(2339)
}
Enter fullscreen mode Exit fullscreen mode

What are other options for isCat implementation? I can think of

function isCat(animal: Cat | Dog): animal is Cat {
    return animal.hasOwnProperty('numberOfLives');
}
Enter fullscreen mode Exit fullscreen mode

This works, but numberOfLives is a plain string. It's easy to make a typo or forget to change this string in the process of refactoring. Is there a better option to create the type predicate?

Collapse
karataev profile image
Eugene Karataev

All right, I found that TS has discriminated unions for such cases.
Just extend your interfaces with a common property and use it to narrow types in your code.

interface Cat {
  kind: 'cat';
  numberOfLives: number;
}
interface Dog {
  kind: 'dog';
  isAGoodBoy: boolean;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return animal.kind === 'cat';
}

function getAnimal(): Cat | Dog {
  return {
    kind: 'cat',
    numberOfLives: 7
  }
}

let animal = getAnimal();
animal.numberOfLives // Error. Property 'numberOfLives' does not exist on type 'Cat | Dog'

if (isCat(animal)) {
  animal.numberOfLives // OK
}

Enter fullscreen mode Exit fullscreen mode
Collapse
mirkorainer profile image
Mirko Rainer

I use the "as" keyword and then a specific property on the type.

export function isWeapon(item: Item | Weapon | Armor | Shield): item is Weapon {
    return (item as Weapon).damageDice !== undefined;
}
Enter fullscreen mode Exit fullscreen mode

so in your above example you could leave out the "kind" property:

interface Cat {
  numberOfLives: number;
}
interface Dog {
  isAGoodBoy: boolean;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).numberOfLive !== undefined;
}
Enter fullscreen mode Exit fullscreen mode

I like not having to add the extra property to my types. :)

Full code for my example found in this file: github.com/mirkoRainer/RulesLawyer...

Collapse
beraliv profile image
beraliv

Would be good to cover if we can create Predicate type which will infer type based on some value in it:

type A = { a: 1 } | { a: 2; b: 2 } | { a: 3; b: 3; c: 3 }

// expect { one: { a: 1 }; two: { a: 2; b: 2 }; three: { a: 3; b: 3; c: 3 } };
type B = {
  [K in A['a']]:  // implementation
  // infer { a: 1 } if K equals 1
  // infer { a: 2; b: 2 } if K equals 2
  // infer { a: 3; b: 3; c: 3 } if K equals 3
  // But with using only A, not repeating the part of it
}
Enter fullscreen mode Exit fullscreen mode

Maybe sounds like another article