Looking for Senior AWS Serverless Architects & Engineers?
Let's TalkTypescript uses a structural type system which means when comparing types, Typescript looks at the structure of the types to decide if they are “assignable” to each other.
We can think of a "type" as a set of all the values that can be assigned to it. In this blog, we will be conceptualizing the type system as a universe that consists of all the possible types and their values.
If you were to group all the string values in the universe, you could define them using something like
All the string values in the universe are now “assignable” to the type MyType.
If you want to limit your new type to a specific set of string values, you would explicitly state them in your type definition.
Only two values in the type universe are now “assignable” to the type MyType.
Before we move on, let’s get a quick refresher on Javascript primitive and object data types as we’ll use these terms later.
From the MDN Docs:
JS Primitives
A primitive (primitive value, primitive data type) is data that is not an object and has no methods or properties. There are 7 primitive data types: string, number, bigint, boolean, undefined, symbol, and null.
JS Object
Objects can be seen as a collection of properties. With the object literal syntax, a limited set of properties are initialized; then properties can be added and removed. Property values can be values of any type, including other objects, which enables building complex data structures. Properties are identified using key values. A key value is either a String value or a Symbol value.
Primitive Type
A primitive type in Typescript is a type representing a javascript primitive. For example, all the string values in javascript can be assigned to the string primitive type.
Unit Type
A unit type is a type that has only one value that can be assigned to it.
Every primitive value in javascript is also a unit type. “4”, 56, “cat”, undefined, true, etc can be considered as a unit type with only one value.
In the above examples, each type can only be assigned one value which is the type itself. The type Cat can only be assigned to the value "cat" , similarly, the type Number4 can only be assigned to the value 4 . These types are thus called unit types.
Let’s look at the following type.
We can also think of it as if the type MyType is made up of the union of two unit types ‘hello’ and ‘world’;
The only value that can be assigned to union type "hello" is value "hello" and to union type "world" is value "world" . So a union of these unit types is also the union of their values which can now be assigned to the new type MyType.
Typescript type system is all about “assignability”. It doesn’t care about the names of the type as long as the value is assignable to another type. This is also referred to as “duck typing”
"If it looks like a duck, walks like a duck, and quacks like a duck, then it is a duck."
Type Unions
The pipe symbol | is similar to an “or” operator. In the context of a type universe, any type to the left or the right side of the | symbol is assignable to the new type.
The | operator works exactly the same as a union of two sets.
The union of two sets is a set containing all elements that are in A or B (possibly both). For example, {1,2} ∪ {2,3} = {1,2,3}
The values (from the universe) that can be assigned to type Animal are ‘lion’ or ‘tiger’.
The values (from the universe) that can be assigned to type Insect are ‘ant’ or ‘bee’.
If we were to create a new type called Living and wanted both the values of Animals and Insect to be assignable to it. You’d define it like this.
This means any value assignable to type Animal or any value of type Insect is also assignable to the type Living.
Quiz #1
What are all the values that are assignable to each of the following types?
Type Intersection
The ampersand symbol & is similar to an “and” operator. In the context of a type universe, any value that can be assigned to both the left and the right side of the & symbol is assignable to the new type.
The & operator works exactly the same as an intersection of two sets.
The intersection of two sets A and B , denoted by A ∩ B , consists of all elements that are both in A and B. For example, {1,2} ∩ {2,3} = {2}
Continuing our previous example.
The unit types on the left side are ‘ant’ or ‘bee’ while the unit types on the right side are ‘bee’ or ‘cow’. We need to find a value in the universe that can be assigned to both Insect and Domestic. There’s only one such value and that is 'bee' as it is assignable in both.
This can also be evaluated by applying distributive law.
So the new type DomesticInsect has only one value and that is 'bee'.
What about the following types?
Since there is no common valu sides, DomesticAnimal is a type with no assignable value and the type with no value is called the never type.
Let’s break it down further by applying distribution.
Note that if you intersect a subset type with a superset, then the resulting type is the subset itself. Can you think why?
For example, In the example below, Admin is a more specific type (also called a narrower type or a subset) compared to Name, which is a type of all string values.
Type string consists of all the possible string values in the universe including dan and joe. Whereas the type Admin only consists of two values dan and joe . When we apply the & operator to find the intersection, the only two values common in both the types are dan and joe.
The same principle applies when you take the union of a subset type with a superset, then the resulting set is the superset.
The above type E evaluates the type string as it includes all the possible string including the values and dan and joe
Quiz #2
What values can be assigned to each of the following types?
Object Types
In the type universe analogy, an object type like interface User { name: string; } is a set of all the objects in the universe that have a field called name.
How many such objects would exist in the universe? Infinite.
So { name: 'John' } , { name: 'Dog', type: 'Animal' } , { name: 'Microsoft', employees: 200000 } , { name: 'Henna', id: 3, age: 20 } are all assignable to the type User above.
One thing that confused me in my early days with Typescript was how come an object of type UserWithAge is assignable to User, but assigning a literal value of { name: 'Jim', age: 25 } to a variable of type User does not work.
It turns out typescript has a special rule for these scenarios. You might have come across a typescript error message that looks like Object literal may only specify known properties
This is the rule that prevents assigning an object literal ({ name: 'Jim', age: 13 }) to a type User if it has a property (age here) that is not present in the type. This rule only applies when you are assigning it to a an object literal.
Note that { name: 'Jim', age: 13 } is actually assignable to the type User as per the assignability rule, it’s just that you can’t assign it if it’s literal.
This constraint was added to Typescript 1.6. Before that it used to work fine. You can see more details HERE
Type Unions
Object union is where things get little tricky. Often times when doing a union of two object types, people do a union of their properties.
For example, for the User and Admin type above, the wrong way of doing their union is to do a union of their properties.
The correct way to do a union of two or more object types is to do a union of all the values that are assignable to these objects.
Remember the infinite values that are assignable to any object?
That means any value in the type universe that is assignable to User or any value that is assignable to Admin is assignable to User | Admin.
When you write the above code in a TS Intellisense, you would see that it only suggests two properties from the above union.
This is because when Typescript comes across the union symbol (|) it has to decide on what fields can be accessed without exactly knowing if the entity is a User or an Admin
Typescript Intellisense, in that case, only allows fields that exist on both the object types. After all, the entity argument could either be a value of type User or Admin. We don’t know specifically what it is until we narrow it down to either one of them.
Here’s the TS playground link to try it. If you try to access any other field if would complain.
This is a valid constraint enforced by the Typescript Intellisense since we can only guarantee that out of all the infinite possible values that are assignable to User | Admin the two fields that will be present in all of those values would be name and id as they are present in both the object types.
It can be misleading to think that since the Typescript Intellisense only suggests name and id fields in the resulting type (Name | Admin), the resulting type might only have those two fields.
Narrow Down The Type
Since we know that all the values of the User type will have a field named active which the values of type Admin won’t have, we can use this unique property to distinguish between the two types. This is also called Discriminated Union.
Here’s the playground link to try the above.
Type Intersections
Let’s take the same example from above to go through object type intersection. The only difference is that this time in our function, we expect a User & Admin object rather than a union of the two.
A very common but wrong way to think of object intersection is to simply get an intersection of the fields of the given types.
The correct way to do an intersection of two or more object types is to do an intersection of all the values that are assignable to these objects.
Let’s take an example to understand this better. In the image below, we have a bunch of objects that are assignable to the type User, and in the image after it, we have objects that are assignable to the type Admin. Note that we cannot show all objects that can be assigned to each type as there are infinite of such objects.
In the image below, if you look at the highlighted objects, they are assignable to both User and Admin since they have properties of both types. We can ignore the extra properties on them.
Objects similar to the highlighted ones above are the result of an intersection of two object types. These objects have all the properties of the type User and also have all the properties of the Admin.
In other words, the intersection of two or more object types is a set of all the objects that have all the properties of all the objects being intersected.
This may seem a little weird because an intersection of the two types User and Admin looks like a union of their properties. But the premise of this behaviour is that intersection of two object types is not the intersection of the fields of the two types, it’s the result of the intersection of all the values assignable to a type with all the values assignable to the other type.
Since intersection always produces a narrower type, in this case also, the resulting intersection is a more specific type than the intersected object types as it has more properties than either of the parent types.
Typescript Intellisense also suggests all the properties of the intersected objects.
Here’s the playground link to try it.
Intersection With Any
If we look at the following typescript code.
Intuitively thinking, number & any should result in the type number since number seems to be a subset of type any. In other words, a number is a narrower type than any, which is the broadest type.
Interestingly, this is not how it works. I was surprised when I learned that.
Independently, a number is a narrower type than any. Since any is assignable to all values and number can only be assigned to numeric values. But the behaviour changes when they are brought into the same context. In other words, whenever a type (e.g. number) is intersected with any, any behaves as a subtype i.e a narrower type which results in the output of the that intersection to be any.
An intersection between a subtype and a supertype always results in the subtype.
This might be a little unintuitive since we expect any to be a wider type rather than a narrower one.
Conclusion
If you love Javascript, Typescript will only make you love it even more. I’ve seen many success stories with using Typescript. We recently migrated a large project to typescript, and we haven’t seen a code syntax or a type error ever since we went to prod.
Hope you learned something from the article. If you have any questions or feedback free to reach out to me @ tushar.sharma@serverlessguru.com