Functional Programming with Type Guards
Typescriptadvanced typesTypescript is impressive in what it can infer about types. I am impressed almost every day what Typescript can infer. Let's look at an example that impressed me this week.
interface InsertEvent {
type: "insert";
newData: Customer;
}
interface ChangeEvent {
type: "change";
newData: Customer;
oldData: Customer;
}
interface DeleteEvent {
type: "delete";
oldData: Customer;
}
type DataEvent = InsertEvent | ChangeEvent | DeleteEvent;
const events: DataEvent[] = [
{ type: "insert", newData: customer1 },
{ type: "delete", oldData: customer3 },
];
function isInsert(event: DataEvent): event is InsertEvent {
return event.type === "insert";
}
events.filter(isInsert).map((event) => {
// The Typescript compiler knows that event is now
// an InsertEvent. It will only you to access the `type` and
// `newData` properties.
return event.newData.name;
});
There are a couple of interesting Typescript language features in this code.
Discriminated Union #
The DataEvent
type in the above code is a discriminated union. It is a discriminated union because we defined three types (InsertEvent
, ChangeEvent
and DeleteEvent
)
all with the same property: type
. type
is called the discriminate. We then union the three types together into a single type: DataEvent
. The discriminate name type
isn't necessary. There is nothing special about type
. We could have named it foo
, and Typescript will still recognize type
as the discriminate. We use the
common property to determine the type of the DataEvent
.
User-Defined Type Guards #
isInsert
is a User-Defined Type Guard. isInsert
tells the Typescript compiler how to know if a DataEvent
is an InsertEvent
. This exciting part about the isInsert
function is the return type. The return type event is Insert
is called a type predicate. This is the special sauce that allows Typescript to narrow the type to an InsertEvent
after it is used as the filter
expression.
Why is this cool? #
We could have defined DataEvent like this.
interface DataEvent {
type: "insert" | "change" | "delete";
newData?: Customer;
oldData?: Customer;
}
The issue with this definition is it doesn't provide any help to the user of the type as to when newData
and oldData
will be undefined
or have a value.
This is a source of bugs waiting to happen. If we can define types that don't rely on data that is sometimes missing, we can eliminate a source of bugs. I was impressed
that in a filter
, map
chain, Typescript could determine that the DataEvent
had been narrowed to an InsertEvent
. I knew that Typescript could do this
for if
blocks inside of methods, but I didn't realize it could do it across method calls.
I am spending a lot of time learning Typescript at work, so stay tuned for more Typescript Tips.
- Next: New Blog