TypeScript — Super-types 💪
This article is part of the collection “TypeScript Essentials”,
this is Chapter five.
In the last article, “Generics and overloads”, we saw some real world usages of types with Generics Types.
We’re going to finish our journey on “real world usages” with some advanced types.
Unions and Intersection types
We already saw some examples of union types in the “Learn the basic” chapter, however, we didn’t talked about the Intersection types.
Intersection types
interface BaseConfig { version: string; name: string; }interface DynamicConfig { fromFile: string; }interface StaticConfig { configuration: object; }type Configuration = (StaticConfig | DynamicConfig) & BaseConfig;
Here, a Configuration
can have 2 different shapes that
share the same BaseConfig
base type.
const config: Configuration = {
version: '1.0',
name: 'myDynamicConfig',
fromFile: './config.json'
}; // this is a DynamicConfigconst config: Configuration = {
version: '1.0',
name: 'myStaticConfig',
configuration: { ... }
} // this is a StaticConfig
Remember that intersections type are useful for composing or overriding existing types.
The “Learn the basic” chapter did not also expose the best use case of union types: the Discriminated Unions.
The Discriminated Unions
Discriminated Unions is a pattern that allow to build types that shares a common property but have different shapes (example: redux actions).
From the TypeScript documentation, we can learn that Discriminated Unions
have three ingredients:
- Types that have a common, singleton type property — the discriminant.
- Then, a type alias that takes the union of those types — the union.
- Finally, a type guard on the common property (on the discriminant).
Example
Here,
- The discriminant is the
type
property, shared by bothCar
andMoto
types. - The
Vehicule
type is the union type. - Finally, the
switch
type guard is based on thetype
property (on the discriminant).
Based on the shape and on the type
discriminant property,
we know that a moto can have 1–2 passengers
([Person] | [Person, Person]
tuples union type).
This pattern is really useful “in the real world”, a concrete example is:
- typing redux reducers (which are basically switches on a action type discriminant property).
Mapped types
While Generic types help you to create “General types”,
Mapped Types will help you transform existing types.
Again, let’s look at what the TypeScript documentation says:
In a mapped type, the new type transforms each property in the old type in the same way.
But, how do we “transform each property”, in other terms, how do we iterate over a type properties?
Let’s take a look at the following keywords: in
and keyof
.
Imagine we have the following User
type:
All those User
properties are required on API side, however, in our single page application, the on-boarding is divided in 2 steps, meaning that the non-saved user record might have undefined
properties, let’s call it theLocalUser
type.
How to create this on-boarding LocalUser
type?
One solution might be to create a copy of the User
type with “nullable” properties:
However, for a maintainability point of view, it’s a shame to duplicate the User
type.
This is when Mapped Types will help us:
Let’s look at this, step by step:
keyof User
will be a type that would be any value inkey of User
, so the following union type:‘first_name’ | ‘last_name’ | ‘city’ | ‘twitter_handle’
K in
will give us a iterator ofUser
properties union type.- Finally,
User[K]
, called a index type, will bring us the type of theK
index ofUser
.
In short, whenK
iscity
, thenUser[K]
will beCityShortFormat
.
Let’s end with a combination of mapped type and generics:
type Partial<T> = { [P in keyof T]?: T[P]; }
type LocalUser = Partial<User>;
We now have a general way to get a “partial subtype” of any type, here: User
.
Conditionals
Conditionals is a new feature from TypeScript 2.8, designed to help us building “non-uniform type mappings”, but what does it mean?
Conditional type enable building more complex types using the ternary operator syntax to perform a type resolution.
Still pretty cryptic, isn’t it?
The best way to understand it is to look to one of the default conditional types predefined by TypeScript (in lib.d.ts
):
Exclude<T, U>
– “Exclude fromT
those types that are assignable toU
.”
Let’s now look at Exclude<T, U>
definition:
and usage:
But how this T extends U
iterate over the keyof User
union type?
This is achieved by what is called Distributive conditional types,
meaning that:
Conditional Types “real world usages”?
While Conditional Types are a really advanced feature that might be useful only “to type” general purpose libraries that use complex APIs (like lodash or recompose), knowing that it exists and how it works may be useful.
Especially depending on the complexity of your project.
Want to know more about Conditional Types?
This article only “scratch the surface” of the subject, for example, I don’t talk about the new infer
keyword.
If you are interested in this subject, I really encourage you to read the following article that will introduce you this infer
keyword and how to use it:
Conclusion
Every-time you end “copy-pasting” types or writing very long ones,
ask yourself:
Is there an advanced typing feature of TypeScript to help me having a more concise or powerful solution?
If you want to dive further into advanced types, I advise you to take a look at the TypeScript documentation — that contains deeper explanations and even more advanced concepts:
You prefer “learn by example”? Then, you should definitely take a look at the SimplyTyped repository that centralise dozen of advanced types: