Combining types with intersection types

Sometimes, it's important for us to have the ability to handle a case where we can bring multiple types together and treat them as one type. Intersection types are the types that have all properties available from each type that is being combined. We can see what an intersection looks like with the following simple example. First of all, we are going to create classes for a Grid along with a Margin to apply to that Grid, as follows:

class Grid {
Width : number = 0;
Height : number = 0;
}
class Margin {
Left : number = 0;
Top : number = 0;
}

What we are going to create is an intersection that will end up with Width and Height from the Grid property, along with Left and Top from Margin. To do this, we are going to create a function that takes in Grid and Margin and returns a type that contains all of these properties, as follows:

function ConsolidatedGrid(grid : Grid, margin : Margin) : Grid & Margin {
let consolidatedGrid = <Grid & Margin>{};
consolidatedGrid.Width = grid.Width;
consolidatedGrid.Height = grid.Height;
consolidatedGrid.Left = margin.Left;
consolidatedGrid.Top = margin.Top;
return consolidatedGrid;
}

Note, we are going to come back to this function later in this chapter when we look at object spread to see how we can remove a lot of the boilerplate copying of properties.

The magic that makes this work is the way we define consolidatedGrid. We use & to join together the types we want to use to create our intersection. As we want to bring Grid and Margin together, we are using <Grid & Margin> to tell the compiler what our type will look like. We can see that we don't have to explicitly name this type; the compiler is smart enough to take care of this for us.

What happens if we have the same properties present in both types? Does TypeScript prevent us from mixing these types together? As long as the property is of the same type, then TypeScript is perfectly happy for us to use the same property name. To see this in action, we are going to expand our Margin class to also include Width and Height properties, as follows:

class Margin {
Left : number = 0;
Top : number = 0;
Width : number = 10;
Height : number = 20;
}

How we handle these extra properties really depends on what we want to do with them. In our example, we are going to add Width and Height of Margin to Width and Height of Grid. This leaves our function looking like this:

function ConsolidatedGrid(grid : Grid, margin : Margin) : Grid & Margin {
let consolidatedGrid = <Grid & Margin>{};
consolidatedGrid.Width = grid.Width + margin.Width;
consolidatedGrid.Height = grid.Height + margin.Height;
consolidatedGrid.Left = margin.Left;
consolidatedGrid.Top = margin.Top;
return consolidatedGrid;
}

If, however, we wanted to try and reuse the same property name but the types of those properties were different, we can end up with a problem if those types have restrictions on them. To see the effect this has, we are going to expand our Grid and Margin classes to include Weight. Weight in our Grid class is a number and Weight in our Margin class is a string, as follows:

class Grid {
Width : number = 0;
Height : number = 0;
Weight : number = 0;
}
class Margin {
Left : number = 0;
Top : number = 0;
Width : number = 10;
Height : number = 20;
Weight : string = "1";
}

We are going to try and add the Weight types together in our ConsolidatedGrid function:

consolidatedGrid.Weight = grid.Weight + new          
Number(margin.Weight).valueOf();

At this point, TypeScript complains about this line with the following error:

error TS2322: Type 'number' is not assignable to type 'number & string'.
Type 'number' is not assignable to type 'string'.

While there are ways to solve this issue, such as using a union type for Weight in Grid and parsing the input, it's generally not worth going to that trouble. If the type is different, this is generally a good indication that the behavior of the property is different, so we really should look to name it something different.

While we are working with classes in our examples here, it is worth pointing out that intersections are not just constrained to classes. Intersections apply to interfaces, generics, and primitive types as well.

There are certain other rules that we need to consider when dealing with intersections. If we have the same property name but only one side of that property is optional, then the finalized property will be mandatory. We are going to introduce a padding property to our Grid and Margin classes and make Padding optional in Margin, as follows:

class Grid {
Width : number = 0;
Height : number = 0;
Padding : number;
}
class Margin {
Left : number = 0;
Top : number = 0;
Width : number = 10;
Height : number = 20;
Padding?: number;
}

Because we have provided a mandatory Padding variable, we cannot change our intersection, as follows:

consolidatedGrid.Padding = margin.Padding;

As there is no guarantee that the margin padding will be assigned, the compiler is going to do its best to stop us. To solve this, we are going to change our code to apply the margin padding if it is set and fall back to the grid padding if it is not. To do this, we are going to make a simple fix:

consolidatedGrid.Padding = margin.Padding ? margin.Padding : grid.Padding;

This strange-looking syntax is called the ternary operator. This is a shorthand way of writing the following—if margin.Padding has a value, let consolidatedGrid.Padding equal that value; otherwise, let it equal grid.Padding. This could have been written as an if/else statement but, as this is a common paradigm in languages such as TypeScript and JavaScript, it is worth becoming familiar with.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.62.45