This is a premium alert message you can set from Layout! Get Now!

Understanding discriminated union and intersection types in TypeScript

0

Understanding the thought pattern behind the union and the intersection types in TypeScript helps you learn how better to implement them when necessary.

In this article, we will explore the concept of union and intersection; first by looking at the mathematical definition of these concepts used in set theory; then by examining the definition and usage of the concept of union and intersection in TypeScript.

Union and intersection in set theory

Set theory provides the implicit concept that most of us use when thinking about union and intersection.

Here are two sets of numbers arranged in ascending order: D = { 0, 1, 2, 3 } and B = { 1, 2, 3, 4, 5 }.

This below definition explores what union and intersection are in relationship to the above defined sets:

The union of the sets D and B, denoted as D ⋃ B, is a set of all elements that are included in (contained in) either D, or B, or both. The union of the set D and the set B is, { 0, 1, 2, 3, 4, 5 }.

Intersection of the sets D and B, denoted as D ⋂ B, is a set of all elements that are in both D and B. The intersection of the set D and the set B is, { 1, 2, 3 }.

Most approaches when trying to understand the above concepts comes from this definition and usage in set theory.

Union and intersection types in TypeScript

In TypeScript, union and intersection types are used to compose or model types from existing types.

These new composed types behave differently, depending on whether they were composed through a union or intersection of the existing type they were formed from.

Defined in the Advanced Types section of Typescript, an intersection type is a type that combines several types into one; a value that can be any one of several types is a union type.

The & symbol is used to create an intersection, whereas the | symbol is used to represent a union. An intersection can be read as an And operator and a union as an Or.

Let’s explore further how this applies to our TypeScript code.

Using primitive types in computer science, a type’s values are its instances. Let’s look at a hypothetical type, Char, which holds no properties, but still represents an interval of values, as seen here:

char = { -128, ..., 127 } // Char type from -128 to 127
unsignedChar = { 0, ..., 255 } // UnsignedChar type from 0 to 255

The intersection of the above two types gives us a new type:

// typescript
type PositiveChar = Char & UnsignedChar // unsignedChar ∩ char <=> { 0, ..., 127 }

The union of the above two types gives us another new type that is able to hold both string and number:

// typescript

string // any char array 
number // any number 

type PrimitiveType = string | number

The most common mistake is to believe that a type’s values are its characteristics (properties) — rather, the values of a type are its instances.

As a result, the intersection has fewer values than its components and can be used in any function that is designated for them.

Similarly, union produces a new, less rigid, more open type — it should be noted that you can’t be certain that the functions associated with this new type are still valid.

Now, let’s follow up the above definitions with how we can implement union and intersection in our TypeScript code.

Union types

You will want to write a function that expects a parameter to be either a number or a string and executes a part of the function code depending on the argument passed to it.

Take, for example, the following function:

function direction(param: any) {
  if (typeof param === "string") {
    ...
  }
  if (typeof param === "number") {
    ...
  }
  ...
}

In the above code, we expect an argument with the type either as string or number, which forms a new type, but we have a problem with the above implementation.

With the type any, we can end up being able to pass types that are not string or number; but we want to strictly make the function exclusively accept a string or number type.

With the help of union, we can re-implement the above code snippet to receive the value of a string or number instance exclusively.

function direction(param: string | number ) {
  ...
}

We can extend our above value type to involve more primitive types like a boolean value, and if we passed value instances beyond our union type definition such as a boolean, the code panics with an error message like this:

Argument of type 'boolean' is not assignable to parameter of type 'string | number'.

To aid the compiler, we take our union definition up a little further, being specific with the type definition. This involves the use of the literal value of a type when defining a new type.

// TypeScript
type LoadingState = {
  state: "loading";
};
type FailedState = {
  state: "failed";
  status: number;
};
type SuccessState = {
  state: "success";
  response: {
    isLoaded: boolean;
  };
};

When we use the above type definition to compose a new type and use it against a pattern-matching process within our code, we can easily learn what part of our type we are trying to access.

type State = LoadingState | FailedState | SuccessState;

function request(state: State): string {
  switch (state.state) {
    case "loading":
      return "Uploading...";
    case "failed":
      return `Error status: ${state.status}, while Uploading`;
    case "success":
      return `Uploaded to cloud: ${state.response.isLoaded} `;
  }
}

Close observation would show how we’re helping the compiler make decisions depending on what part of the code we’re executing.

Trying to call the state.state.status outside the switch will raise a warning that property doesn’t exist on the type, but it’s safe trying to access the status field when executing it under the case "failed" switch statement.

Intersection types

An easy way to understand intersections involves error handling.

Imagine you want to implement a type for reading and writing to a file. We handle errors that could occur when we try to read or write to a file because this way, you enforce that such types contain a type for handling the error or success while reading and writing to a file.

interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface File {
  content: { lines: string }[];
}

type FileReader = File & ErrorHandling;

const writeToAFile = (response: FileReader) => {
  if (response.error) {
    console.error(response.error.message);
    return;
  }

  console.log(response.content);
};

Intersection types can only contain a subset of their components’ instances, but they can use any of their functions. An intersection type combines multiple types into one.

Conclusion

This is good approach to follow when trying to understand union and intersection in TypeScript.

We can combine numerous types into one with an intersection type. The structure of an object with an intersection type must include all of the kinds that make up the intersection types; it’s made up of various types joined together by an & symbol.

Union types produce a new type that allows us to create objects with some or all of the properties of each of the types that make up the union type. The pipe | symbol is used to join multiple types to form union types and allows us to create a new type that inherits some of the structure of union types.

The post Understanding discriminated union and intersection types in TypeScript appeared first on LogRocket Blog.



from LogRocket Blog https://ift.tt/hb1ZMLp
via Read more

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top