Narrow Interface Type in TypeScript

If you narrow an interface in Typescript, you can operate on it more specifically. And you can keep your type safety.
Multiple Types
Narrowing types requires first the challenge of having two or more types. Let's say you have two interfaces:
interface Legacy {
render(): void
}
interface Current {
update(): void
}
In this scenario, these two types represent the same kind of thing. It's a program -- either a legacy version or the current version. They have different APIs. One's an object with a render function, the other with an update. So, let's define them as related:
type Program =
| Legacy
| Current
Now we have a challenge to deal with.
Distinguishing Types
In our code, we want to use the programs similarly, whether it's the legacy or current version. So, let's say we have a function:
function run(program: Program) {
if (isLegacy(program))
program.render()
else
program.update()
}
Either version of the program will run, but they're kicked off with a different API.
So, the magic question is: What's inside the isLegacy function that allows TypeScript to keep us type safe?
Narrowing Types with a Type Predicate
isLegacy must know how to narrow the types. We narrow in on which version of type Program it really is. It's not enough to know that it's a Program; that's too broad. We need to know if it's specifically Legacy or Current.
Here's an example implementation of isLegacy and how this might be done:
function isLegacy(program: Program): program is Legacy {
return (program as Legacy).render !== undefined
}
There's a lot of typing happening here. Here are the important points:
- The input is the union type,
Program. Coming in,programcould be either version. - The return type is a type predicate (
program is Legacy). The nameprogrammatches the function parameter name, followed by theiskeyword. - Internally, program is cast
as Legacyso the TypeScript typechecker can be happy with.renderbeing called on it.
And remember how this was used:
if (isLegacy(program))
program.render()
Inside that conditional, after the true evaluation of isLegacy, calling functions that are only available on the Legacy version of the program is safe.
Narrowing Types with the in Keyword
There's another way that type narrowing is possible. Instead of using the type predicate, we could use the in keyword, testing for the existence of the distinguishing property.
Only the Legacy version of Program has a render function, so we'll test for it:
if ("render" in program)
program.render()
The nice thing about this method is that it's a little terser and not so much typing in the syntax.
What other ways have you used to narrow types in your TypeScript programs?