TypeScript — Super-types 💪

Charly Poly
6 min readNov 3, 2018

--

https://unsplash.com/photos/6yjAC0-OwkA

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 DynamicConfig
const 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:

  1. Types that have a common, singleton type property — the discriminant.
  2. Then, a type alias that takes the union of those types — the union.
  3. Finally, a type guard on the common property (on the discriminant).

Example

Here,

  • The discriminant is the type property, shared by both Car and Moto types.
  • The Vehicule type is the union type.
  • Finally, the switch type guard is based on the type 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:

LocalUser is just the User type with same optional properties.

Let’s look at this, step by step:

  • keyof User will be a type that would be any value in key of User, so the following union type: ‘first_name’ | ‘last_name’ | ‘city’ | ‘twitter_handle’
  • K in will give us a iterator of User properties union type.
  • Finally, User[K], called a index type, will bring us the type of the K index of User.
    In short, when K is city, then User[K] will be CityShortFormat.

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 from T those types that are assignable to U.”

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:

--

--