March 3, 2022
Making Union types click
Typescript can get a bit tricky as we start diving into some of the more advanced Types such as Unions, Intersections, and Mapped Types. This is when creating a solid foundation for how we think about Types can save a lot of frustration. A great way to think about how Types work, is to consider what the âdomainâ of a Type consists of. In other words, all the possible values that could be assigned to a given variable. Letâs start with dissecting one of Typescripts primitive Types and expand from there.
If you assign the Type âbooleanâ to a property, Typescript checks the value against anything in the domain of a boolean type. Ok, so what is the domain in this example? Imagine we wrote âtrueâ on a piece of paper and put it in a hat, then we wrote âfalseâ on a piece of a paper and put it in the same hat. The paper in the hat represents the domain of the boolean type. The same mental model can be used for a value assigned to something else like a ânumberâ type. Instead of only 2 pieces of paper in the hat, the domain would contain every possible value of a number*.
*There are limitations to what a number can represent in Typescript, but we wonât be getting into that within the scope of this post. Also depending on what settings you are using for strict NullChecks, you would have to consider if undefined and null also fall into the domain of that Type.
The reason it helps to think about types in this way is because it clears up questions that will be encountered as more advanced types are utilized. A common misconception is what a Union Type actually represents according to Typescript. Letâs use this mental model to try and finally make Union types âclickâ.
Creating Unions
Typescript gives us the ability to use something called âoperatorsâ. Operators are built into the Typescript language and allow us to combine existing Types to create entirely new Types. There are many kinds of operators in Typescript, and the union operator is one of the more popular ones to understand. The union operator is implemented via the vertical pipe symbol on our keyboards.
type MyFirstUnion = string | number;
Applying the union operator to two or more existing types will create whatâs called a Union type. The code example above creates a new Union type called âMyFirstUnionâ. Now, recall from earlier that a type simply defines all the possible values that can be assigned to a given variable. With that in mind, lets break down exactly what is in the âdomainâ of âMyFirstUnionâ.
Domain of MyFirstUnion (assuming strict null checking is ON):
- any string value
- any number value
This is fairly easy to reason about and means that we can apply that type to a property, and it will enforce that the value is going to be either a string or a number.
const id1: MyFirstUnion = "2"; //All good!
const id2: MyFirstUnion = 2; //All good!
const id3: MyFirstUnion = {}; //Type '{}' is not assignable to type 'MyFirstUnion'
Here we can see that Typescript will throw and error since an empty object is not contained in the domain of âMyFirstUnionâ.
Something that is important to understand about union types is you can only use javascript operations on those values that are valid for EVERY member of the union. Typescript sees that weâve assigned a variable as âMyFirstUnionâ, but it doesnât know at any given moment if the value is indeed a string or a number. All it understands is that the value assigned must be within the domain of that type. So, for example if we wanted to perform an operation that is only valid on a string value, we would get an error!
function getLength(id: MyFirstUnion) {
return id.length;
// Property 'length' does not exist on type 'MyFirstUnion'.
// Property 'length' does not exist on type 'number'.
}
This might be surprising at first, but once we really grasp what a union type does, it makes total sense! In the above getLength function, what would happen if we passed a number value? During run time of our function, it would return âundefinedâ which is probably not what we would want!
This rule is also true for any dot operations on an object. Dot operators are used to access properties of an object and are generally used like:
const house = {
rooms: 3,
};
console.log(house.rooms); // 3
If we try to access a property of a union type, it will only allow us to perform that operation if itâs valid for ALL members of the union. Take this new union type composed of two existing types.
interface Home {
rooms: number;
}
interface Extras {
backyard?: boolean;
}
type HomeDetails = Home | Extras;
function logRooms(home: HomeDetails) {
console.log(home.rooms);
// Property 'rooms' does not exist on type 'HomeDetails'.
// Property 'rooms' does not exist on type 'Extras'
}
With this in mind, it might not seem overly useful to apply union types when Typescript doesnât even provide useful auto-completions or type safety. However, fear not! There are some nifty ways we can ânarrowâ these union types so that Typescript is aware of exactly which of the unionâd types we want to use.
Working with Unions
When working with union types, often times you must check what the assignment of some variable is before being able to use it. This can sometimes be annoying in practice if we overuse unions.
A good rule of thumb in my opinion is to only introduce unions when a single variable could contain different typed values, and those values are semantically similar. Unions are not a good use-case when you reassign a variable later on with a different type if that value represents an entirely different meaning than the first. Ok, to make that clear letâs look at a concrete example of what NOT to do:
let userId: string | number = 'KarenLovesTheMall32@aol.com';
//some other code
userId = 12345 //reassign userId to a number
still writingâŚ(Chatting with George R. R. Martin & Patrick Rothfuss)