Categories
JavaScript TypeScript

TypeScript Advanced Types — Literal Types and Discriminated Unions

Spread the love

TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There’re multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at intersection and union types.

Intersection Types

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s denoted by an & sign. All members of all the types are required in the object of an intersection type.

For example, we can use the intersection type like in the following code:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
}

let employee: Animal & Person & Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20,  
  employeeCode: '123'  
}

As we can see from the code above, each type is separated by an & sign. Also, the employee object has all the properties of Animal , Person , and Employee . Each property has a type that’s defined in each interface. If the structure doesn’t match exactly, then we’ll get error messages like the following from the TypeScript compiler:

Type '{ kind: string; firstName: string; lastName: string; age: number; }' is not assignable to type 'Animal & Person & Employee'.Property 'employeeCode' is missing in type '{ kind: string; firstName: string; lastName: string; age: number; }' but required in type 'Employee'.(2322)input.ts(12, 3): 'employeeCode' is declared here.

The error above occurs if we have the following code:

let employee: Animal & Person & Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20    
}

TypeScript looks for the employeeCode property since the employeeCode property is in the Employee interface.

If 2 types have the same member name but different type, then it’s automatically assigned the never type, when they’re joined together as an intersection type. This means that we can’t assign anything to it. For example, if we have:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
  age: string;  
}

let employee: Animal & Person & Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20  
}

Then we get the following error from the TypeScript compiler:

Type 'number' is not assignable to type 'never'.(2322)input.ts(8, 3): The expected type comes from property 'age' which is declared here on type 'Animal & Person & Employee'

If we omit the property then the compiler will also raise an error about the age property being missing. Therefore, we should never have types that have the same member name if we want to create intersection types from them.

Union Types

Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol.

For example, we can define an object that has a union type like in the following code:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20    
}

The code above has an employee object of the Animal | Person | Employee type which means that it can have some of the properties of the Animal, Person, or Employee interfaces. Not all of them have to be included, but if they’re included, then the type has to match the ones in the interface.

With union types, we can have 2 types that have the same member name but with different types. For example, if we have the following code:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
  age: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: '20'  
}

Then we can assign both a number or a string to the age property. This fits with the dynamic nature of JavaScript while letting us assign data types to objects. This is different from traditional object-oriented code where we may abstract common members into a parent class and then derive sub-classes from it which has specialized members.

The pipe symbol means the object of a union type can take on none, some, or all properties of each type.

Accessing members in an object of a union type is different from how members are accessed from an intersection type. Objects that have intersection types have to have all the properties listed in the members of each type, so logically, we can access all the properties that are defined in the object. However, this isn’t the case with union types since some members may be available to only some of the types that make up the union type.

If we have a union type, then we can only access members that are available in all the types that form the union type. For example, if we have:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
  age: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: '20'  
}

console.log(employee)

Then we can’t access any properties of the employee object since none of the members are available in all the types. If we try to access a property like the kind property, we’ll get the following error:

Property 'kind' does not exist on type 'Animal | Person | Employee'.Property 'kind' does not exist on type 'Person'.(2339)

If we want to make some property accessible, we can write something like the following code to let us access a property:

interface Animal {  
  kind: string;  
}

interface Person {  
  kind: string;  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  kind: string;  
  employeeCode: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20,  
  employeeCode: '123'  
}

console.log(employee.kind)

In all 3 interfaces above, we have the kind member. Since they’re in all 3 interfaces, which we used in the union type, we can access the employee.kind property. Then we would get the text ‘human’ in the console.log statement.

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s formed by joining multiple types by an & sign. Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol. It lets us create a new type that has some of the structure of each type that forms the union type.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *