Let me show you how creative use of TypeScript’s discriminated unions, type aliases, and functions can give you a greater degree of flexibility in your own code.
I’m going to do this by illustrating how these techniques addressed a problem that I was trying to solve and then talk about some additional ideas on how these techniques can be applied.
The Problem I’m Solving
I’m building a text-based game for a few talks I’ll be giving this spring. This is the type of game where the game engine describes something and then the player types in a command such as
look at the flowers or
hit the mushroom with the glowing red hammer.
In order to do that, I need to be able to represent parts of the game world as objects that have basic responses to verbs that the player can try.
This is fairly easily represented in a simple object where verbs with custom responses are defined as string properties on the object (note: the player is a dog in my game):
So, using the above object, the engine will give default responses to verbs that are not defined on that object (e.g. eat or pick up).
But what if I wanted a verb to actually impact the game world? I’d need something a bit more flexible.
This is where discriminated unions come into play.
What is a Discriminated Union?
Discriminated unions in TypeScript are a way of telling TypeScript that something is going to be one of a fixed number of possibilities.
For example, if I declare a parameter as
x: string | number | boolean, I’m telling TypeScript that it should expect
x to be either a
number or a
boolean. TypeScript will then check my usage of
x to make sure I’m working with properties that are common on each one or doing necessary type checking / casting.
The reason this can be helpful is that it allows you to specify simple paths and complex paths for things depending on what value is passed in.
Defining the Types
Now that we’ve established what discriminated unions are and why they might be helpful, let’s take a look at my definition of a
GameObject. This is an object in the game world that the player can potentially interact with.
Focus first on
ObjectResponse on line 4. This is the type used to represent a verb handler on an object. For example, how a
shoe might respond to the
ObjectResponse as a
ContextAction | string meaning that it will either be a simple string indicating a description to print out or it will be a more complex response represented by
ContextAction is a custom type definition that represents a function taking in a
CommandContext object (a custom domain object representing the current state of the world and some basic response formatting capabilities) and returning
What this is essentially saying is that a verb handler on an object will either return a simple string description or it will be a more complex function that takes in a
CommandContext object and does something with it.
Here’s a practical example that uses the two signatures to do more complex logic in its
look verb responses:
Interpreting Values of Different Types
Okay, so if we’re dealing with something that is a discriminated union, how do we effectively work with it?
Take a look at this TypeScript snippet:
Here we query the
gameObject to see if it has a property named the same thing as
verbName. If that’s not present we’ll just add the generic response for the verb.
If the response is present, we know it to be either a
string or a
ContextAction so we can switch off of the type of that value and handle it appropriately.
Here we respond to
string values by casting the value to string and using a method on the
context to add a simple message.
If the value was not a
string then we know it’s going to be a
ContextAction and will be a function with a signature that takes in a
CommandContext and returns
void. In that case, we simply invoke the function and pass in the
Hopefully this illustrates the usefulness of discriminated unions and type aliases in handling a variety of scenarios and potentially gives you ideas for how to streamline some of your existing logic.
I personally consider discriminated unions and working with functions when I see patterns of similar data or the potential for a high amount of boilerplate or repetitive code.
Discriminated unions do add complexity to your code, but that price can be worth it in simplicity and flexibility in other areas. Ultimately the decision is up to you and will change based on what problems you’re trying to solve.