This blog post will look at Union and Intersection types in TypeScript. It is part of the Introduction to Advance Types in TypeScript series.
Union and Intersection types are useful features of TypeScript. But they can come across as a bit strange, especially to developers coming from more traditional languages like Java, C#, etc. Hence in this post, I will first start with a background section. This will help in developing the fundamental perspective needed to understand and appreciate union and intersection types in TypeScript.
Background: A bit of Set Theory
The important thing is to see type systems from the perspective of Set Theory.
What does this mean?
I’ll explain.
First off, no need to be put off with the mention of set theory. Even though set theory itself is a deep and fundamental topic in mathematics, only the level of set theory learned in high school is required to follow along.
So how does set theory help in understanding types?
In set theory, we have the idea of a set and the elements in the set. A set usually outlines constraints that must be placed on an item before that item can be seen as an element of that set. For example, we can see a fruit basket as a set and the individual fruits in the basket as the element of the set. The constraint being that for an item to be in the basket, it needs to be a fruit.
Mathematicians have a couple of nation for representing sets, one of which is the Set-builder notation. Using the set-builder notation, a set is specified as a selection from a larger set, together with a predicate that specifies the condition an element selected from this larger set, must satisfy to be a member of the set being defined.
An example of such notation is shown below:
{x ∈ M : P(x)}.
This can be interpreted as a Set M, which contains element x, as long as x passes the constraints specified by predicate P.
Expressing our fruits basket in this notation may look like this:
{item ∈ Fruit Basket : As long as item is a fruit}
We also have the idea of set operations in set theory, these are operations that can be performed on, and with sets.
These include operations like finding the number of elements in a set, compliments of a set, union, and intersection operations, etc.
The idea here is to see a Type as a set, and a value as an element in that set.
So instead of just seeing primitive types like string, number, boolean as just facts of life in statically typed languages, the idea is to see them as delineating a set, while their values are the elements contained in that set.
This means seeing any string value as an element in the string set. The same goes for numbers and booleans. The interesting difference between the boolean type when seen as a set is that the element it can contain is finite.
The value of a boolean can either be true or false, hence the boolean type, which we are seeing as a set, only contains 2 elements: the true and false values.
Using the set notation we can write out the boolean set as follows:
{x ∈ boolean: x == true || x == false }
What about types that are created via the definition of custom types? Can we apply the set theory idea also to those? Yes, we can.
For example, given we have the following class Person definition:
Class Person {
private name: string;
private age: number;
constructor(givenName: string, givenAge: number) {
this.name = givenName;
this.age = givenAge;
}
}
Traditionally, most developers will see this just as a blueprint for creating objects. let joe: Person = new Person("Joe", 18);
let mary: Person = new Person("Mary", 20);
etc
Here joe and mary and whatever other objects we create are elements of the Person set. {x ∈ Person: x instanceOf Person }
{x ∈ Person: x.name exist with type string && x.age exist with type number }
So for example if we try to assign the value true to a variable we typed as Person, as shown below:
let doe: Person = true
we get an error: Type 'boolean' is not assignable to type 'Person'
What is Union Type
A can be a set that represents a basket of fruits, while B represents a basket of candies. The type A U B will therefore be a set that contains either a fruit or a candy. A U B. can be aliased as Office Treats if you may. This union operation means if you have a candy, it can go into the A U B set. So also, if you have a fruit, it can go into A U B, but wine, or beer, on the other hand, won’t be an element that can belong to the set A U B.
In TypeScript, union Types are created with the vertical bar: |. So for example a union type based on union-ing a string and number will be created by writing string | number.
For example to define a double function that takes a union of string and number, we have:
function double(input: number | string) {
if (typeof input === "number") {
return input * 2
} else {
return `${input} ${input}`
}
}
console.log(double(3)) // prints 6
console.log(double("hoi")) // prints “hoi hoi”
Flow-Sensitive Typing, Type Narrowing, and Type Guards.
type Stack = {
id: number,
items: string[]
}
type Sound = {
bass: number
tremble: number
volume: number
}
function double(input: Stack | Sound) {
if ("items" in input) {
return {
id: input.id,
items: input.items.concat(input.items)
}
} else {
return {
bass: input.bass,
tremble: input.bass,
volume: input.volume * 2
}
}
}
Here we define two custom types: Sound and Stack, which then allow the use of the in operator as a type guard.
Custom type guards can also be used. This involves using Type Predicates. This will look like the following:
function double(input: Stack | Sound) {
if (isStack(input)) {
return {
id: input.id,
items: input.items.concat(input.items)
}
} else {
return {
bass: input.bass,
tremble: input.bass,
volume: input.volume * 2
}
}
}
// this is the type predicate
function isStack(input: any): input is Stack {
return "items" in input
}
The type predicate above is the isStack function:
function isStack(input: any): input is Stack {
return "items" in input
}
It is used as a predicate to determine if the type of the input value is of a particular type. Notice the return type: input is Stack, this means if the function returns true, then the type of the input can be narrowed to Stack.
This completes the quick introduction of union types, let's take a look at Intersection types next.
What is Intersection Type
A set C which is an intersection of both set A and set C, will therefore be a set that contains both properties of being a fruit and a drink: ie Fruit Juices.
This means any element of an intersection type will have to have properties that belong to the individual types that are involved in the intersection operation that created it.
In TypeScript, an intersection type is created using the ampersand (&)
To further illustrates, given types that defines a developer and system admin as follows:
type DevRole = {
languages: string[],
testing: string[]
}
type SysAdminRole = {
oses: string[],
automation: string[]
}
Then we can construct a DevOps type from these two types as follows:
type DevOps = DevRole & SysAdminRole
Then a value that can be assign the DevOps type can be created as follows:
let andy: DevOps = {
languages: ["java", "JavaScript"],
testing: ["unit", "integration", "functional"],
oses: ["linux", "solaris", "windows"],
automation: ["salt", "ansible"]
}
No comments:
Post a Comment