Type predicates have been around but today I found a particularly nice application. The situation is this: I have an interface that has an optional field, where the presence of that field means I need to create a new object on the server, and the lack of the field means the object has already been created and I'm just holding on to it for later. Here's what it looked like:

interface Thing {
  name: string
  blob?: File
}

const things: Thing[] = [
  /* ... */
]

const uploadNewThings = (things: (Thing & { blob: File })[]) =>
  Promise.all(things.map((thing) => createThing(thing.name, thing.blob)))

The intersection type Thing & { blob: File } means that uploadNewThings only accepts things that have the field blob. In other words, things that need to be created on the server because they have blob content.

However, TypeScript struggles if you try to simply filter the list of things before passing it into uploadNewThings:

uploadNewThings(things.filter((thing) => !!thing.blob))

The resulting error is this long stream of text:

Argument of type 'Thing[]' is not assignable to parameter of type '(Thing & { blob: File; })[]'.
  Type 'Thing' is not assignable to type 'Thing & { blob: File; }'.
    Type 'Thing' is not assignable to type '{ blob: File; }'.
      Types of property 'blob' are incompatible.
        Type 'File | undefined' is not assignable to type 'File'.
          Type 'undefined' is not assignable to type 'File'.

The tl;dr being that despite filtering things by thing => !!thing.blob, TypeScript does not recognize that the return value is actually Thing & { blob: File }.

Now you could just cast it,

things.filter((thing) => !!thing.blob) as (Thing & { blob: File })[]

But casting is bad! It's error-prone and doesn't really solve the problem that TypeScript is hinting at. Instead, use a type predicate:

const hasBlob = (t: Thing): t is Thing & { blob: File } => !!t.blob

uploadNewThings(things.filter(hasBlob))

With the type predicate (t is Thing & ...) I can inform TypeScript that I do in fact know what I'm doing, and that the call to filter results in a different interface.