Advanced TypeScript Programming Projects

5 (1 reviews total)
By Peter O'Hanlon
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Advanced TypeScript Features

About this book

With the demand for ever more complex websites, the need to write robust, standard-compliant JavaScript has never been greater. TypeScript is modern JavaScript with the support of a first-class type system, which makes it simpler to write complex web systems. With this book, you’ll explore core concepts and learn by building a series of websites and TypeScript apps.

You’ll start with an introduction to TypeScript features that are often overlooked in other books, before moving on to creating a simple markdown parser. You’ll then explore React and get up to speed with creating a client-side contacts manager. Next, the book will help you discover the Angular framework and use the MEAN stack to create a photo gallery. Later sections will assist you in creating a GraphQL Angular Todo app and then writing a Socket.IO chatroom. The book will also lead you through developing your final Angular project which is a mapping app. As you progress, you’ll gain insights into React with Docker and microservices. You’ll even focus on how to build an image classification program with machine learning using TensorFlow. Finally, you’ll learn to combine TypeScript and C# to create an ASP.NET Core-based music library app.

By the end of this book, you’ll be able to confidently use TypeScript 3.0 and different JavaScript frameworks to build high-quality apps.

Publication date:
July 2019
Publisher
Packt
Pages
416
ISBN
9781789133042

 

Advanced TypeScript Features

In this chapter, we are going to look at aspects of TypeScript that go beyond the basics of the language. When used appropriately, these features provide a clean, intuitive way to work in TypeScript and will help you to craft professional-level code. Some of the things we cover here may not be new to you, but I am including them so that there is a common baseline of knowledge as we work through later chapters, as well as an understanding of why we will be using these features. We will also cover why we need these techniques; it is not merely enough to know how to apply something, we also need to know in what circumstances we should use them and what we need to consider when we do so. The focus of this chapter is not to create a dry, exhaustive list of each feature—instead, we are going to introduce the information we need to work through the rest of this book. These are practical techniques that we will apply again and again in our daily development.

As this is a book on web development, we are also going to be creating a lot of UIs, so we are going to look at how we can create attractive interfaces using the popular Bootstrap framework.

The following topics will be covered in this chapter:

  • Using different types with union types
  • Combining types with intersection types
  • Simplifying type declarations with type aliases
  • Deconstructing objects with REST properties
  • Coping with a variable number of parameters using REST
  • Aspect-Oriented Programming (AOP) using decorators
  • Composing types using mixins
  • Using the same code with different types and using generics
  • Mapping values using maps
  • Creating asynchronous code with promises and async/await
  • Creating UIs with Bootstrap
 

Technical requirements

In order to complete this chapter, you are going to need Node.js installed. You can download and install Node.js from https://nodejs.org/en/.

You will also need the TypeScript compiler installed. There are two ways to do this through Node.js using the Node Package Manager (NPM). If you want the same version of TypeScript used in all of your applications and are happy that they will all run on the same version whenever you update it, use the following command:

npm install -g typescript

If you want the version of TypeScript to be local to a particular project, type the following in the project folder:

npm install typescript --save-dev

For a code editor, you can use any suitable editor or even a basic text editor. Throughout this book, I will be using Visual Studio Code, a free cross-platform integrated development environment (IDE), available at https://code.visualstudio.com/

All code is available on GitHub at https://github.com/PacktPublishing/Advanced-TypeScript-3-Programming-Projects/tree/master/Chapter01.

 

Building future-proof TypeScript with tsconfig

As TypeScript has grown in popularity, it has benefited from a rapidly evolving open source architecture. The design goals behind the original implementation means that it has proven to be a popular choice for developers, from those who were new to JavaScript-based development to seasoned professionals. This popularity means that the language has quickly gained new features, some straightforward and others geared toward developers who are working on the cutting edge of the JavaScript ecosystem. This chapter aims to address the features that TypeScript has introduced to match either current or upcoming ECMAScript implementations that you might not have encountered previously.

As we progress through this chapter, I will occasionally call out features that require a newer ECMAScript standard. In some cases, TypeScript will already have provided a poly-filled implementation of a feature that works with earlier versions of ECMAScript. In other cases, the version we compile against will have a feature that could not be back-filled beyond a certain point so it will be worth using a more up-to-date setting.

While it's possible to compile TypeScript completely from the command line using nothing but parameters, I prefer to use tsconfig.json. You can either create this file manually or have TypeScript create it for you using the following command from the command line:

tsc --init

If you want to copy my settings, these are the ones I have set up by default. When we need to update references, I will point out the entries that need to be added:

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"lib": [ "ES2015", "dom" ],
"sourceMap": true,
"outDir": "./script",
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"experimentalDecorators": true,
}
}
 

Introduction to advanced TypeScript features

With each release, TypeScript takes big strides forward, adding features and capabilities that build on the basics of the language that were introduced back in version 1. Since then, JavaScript has moved on and TypeScript has added features to target emerging standards, providing implementations for older implementations of JavaScript or by calling native implementations when targeting updated ECMA standards. In this first chapter, we are going to look at some of these features, which we will be using throughout this book.

Using different types with union types

The first feature that we are going to look at is one of my favorites, that is, the ability to use union types. These types are used when a function expects a single parameter to be one type or another. Suppose, for instance, that we have a validation routine that needs to check whether a value is in a particular range and this validation could receive the value either from a textbox as a string value, or as a number value from a calculation. As each of the techniques for solving this issue have a lot in common, we'll start off with a simple class that allows us to specify the minimum and maximum values that form our range and a function to actually perform the validation, as follows:

class RangeValidationBase {
constructor(private start : number, private end : number) { }
protected RangeCheck(value : number) : boolean {
return value >= this.start && value <= this.end;
}
protected GetNumber(value : string) : number {
return new Number(value).valueOf();
}
}

If you haven't seen a constructor that looks like that before, that's the equivalent of writing the following:

 private start : number = 0;
private end : number = 0;
constructor(start : number, end : number) {
this.start = start;
this.end = end;
}

If you need to check your parameters or manipulate them in some way, you should use this expanded format of parameters. If you are simply assigning the values to private fields, then the first format is a very elegant way to do this and saves cluttering up your code.

There are a few ways that we could solve the problem of ensuring we only perform our validation using string or number. The first way we could solve this problem would be by providing two separate methods that accept the relevant type, as follows:

class SeparateTypeRangeValidation extends RangeValidationBase {
IsInRangeString(value : string) : boolean {
return this.RangeCheck(this.GetNumber(value));
}
IsInRangeNumber(value : number) : boolean {
return this.RangeCheck(value);
}
}

While this technique would work, it's not very elegant and it certainly doesn't take advantage of the power of TypeScript. The second technique that we could use is to allow us to pass in the value without constraining it, as follows:

class AnyRangeValidation extends RangeValidationBase {
IsInRange(value : any) : boolean {
if (typeof value === "number") {
return this.RangeCheck(value);
} else if (typeof value === "string") {
return this.RangeCheck(this.GetNumber(value));
}
return false;
}
}

That's definitely an improvement over our original implementation because we have settled on one signature for our function, which means that calling the code is a lot more consistent. Unfortunately, we can still pass an invalid type into the method, so if we passed boolean in, for instance, this code would compile successfully but it would fail at runtime.

If we want to constrain our validation so that it only accepts strings or numbers, then we can use a union type. It doesn't differ much from the last implementation but it does give us the compile time type safety that we're after, as follows:

class UnionRangeValidation extends RangeValidationBase {
IsInRange(value : string | number) : boolean {
if (typeof value === "number") {
return this.RangeCheck(value);
}
return this.RangeCheck(this.GetNumber(value));
}
}

The signature that identifies the type constraints as being a union is type | type in the function name. This tells the compiler (and us) what the valid types are for this method. As we have constrained the input to be number or string, once we have ruled out that the type is not number, we don't need to check typeof to see whether it's a string so we have simplified the code even further.

We can chain as many types together as we need in a union statement. There's no practical limit but we have to make sure that each type in the union list needs a corresponding typeof check if we are going to handle it properly. The order of the types does not matter either, so number | string is treated the same as string | number. Something to remember though is if the function has lots of types combined together, then it is probably doing too much and the code should be looked at to see whether it can be broken up into smaller pieces.

We can go further than this with union types. In TypeScript, we have two special types, null and undefined. These types can be assigned to anything unless we compile our code with the –strictNullChecks option or strictNullChecks = true if we're setting this as a flag in our tsconfig.json file. I like to set this value so that my code only handles null cases where it should, which is a great way to guard against side effects creeping in just because a function receives a null value. If we want to allow null (or undefined), we simply need to add these as a union type.

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.

Simplifying type declarations with type aliases

Something that goes hand in hand with intersection types and union types are type aliases. Rather than cluttering our code with references to string | number | null, TypeScript gives us the ability to create a handy alias that is expanded out by the compiler into the relevant code.

Suppose that we want to create a type alias that represents the union type of string | number, then we can create an alias that looks as follows:

type StringOrNumber = string | number;

If we revisit our range validation sample, we can change the signature of our function to use this alias, as follows:

class UnionRangeValidationWithTypeAlias extends RangeValidationBase {
IsInRange(value : StringOrNumber) : boolean {
if (typeof value === "number") {
return this.RangeCheck(value);
}
return this.RangeCheck(this.GetNumber(value));
}
}

The important thing to notice in this code is that we don't really create any new types here. The type alias is just a syntactic trick that we can use to make our code more readable and, more importantly, help us to create code that is more consistent when we are working in larger teams.

We can combine type aliases with types to create more complex type aliases as well. If we wanted to add null support to the previous type alias, we could add this type:

type NullableStringOrNumber = StringOrNumber | null;

As the compiler still sees the underlying type and uses that, we can use the following syntax to call our IsInRange method:

let total : string | number = 10;
if (new UnionRangeValidationWithTypeAlias(0,100).IsInRange(total)) {
console.log(`This value is in range`);
}

Obviously, this doesn't give us very consistent-looking code, so we can change string | number to StringOrNumber.

Assigning properties using object spread

In the ConsolidatedGrid example from the Intersection types section, we assigned each property to our intersection individually. Depending on the effect that we are trying to achieve, there is another way that we could have created our <Grid & Margin> intersection type with less code. Using a spread operator, we could perform a shallow copy of the properties from one or more of our input types automatically.

First, let's see how we can rewrite our earlier example so that it automatically populates the margin information:

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

When we are instantiating our consolidatedGrid function, this code copies in the properties from margin and fills them in. The triple dots (...) tell the compiler to treat this as a spread operation. As we have already populated Width and Height, we use += to simply add in the elements from the grid.

What happens if we wanted to apply both the values from grid and margin instead? To do this, we can change our instantiation to look like this:

let consolidatedGrid = <Grid & Margin>{…grid, ...margin};

This fills in the Grid values with the values from grid and then fills in the Margin values from margin. This tells us two things. The first is that the spread operation maps the appropriate property to the appropriate property. The second thing this tells us is that the order that it does this in is important. As margin and grid both have the same properties, the values set by grid are overwritten by the values set by margin. In order to set the properties so that we see the values from grid in Width and Height, we have to reverse the order of this line. In reality, of course, we can see the effect as follows:

let consolidatedGrid = <Grid & Margin>{...margin, …grid };

At this stage, we should really take a look at the JavaScript that TypeScript produces out of this. This is what the code looks like when we compile it using ES5:

var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s,
p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function ConsolidatedGrid(grid, margin) {
var consolidatedGrid = __assign({}, margin, grid);
consolidatedGrid.Width += grid.Width;
consolidatedGrid.Height += grid.Height;
consolidatedGrid.Padding = margin.Padding ? margin.Padding :
grid.Padding;
return consolidatedGrid;
}

If, however, we compile the code using the version ES2015 or later, the __assign function is removed and our ConsolidatedGrid JavaScript looks as follows:

function ConsolidatedGrid(grid, margin) {
let consolidatedGrid = Object.assign({}, margin, grid);
consolidatedGrid.Width += grid.Width;
consolidatedGrid.Height += grid.Height;
consolidatedGrid.Padding = margin.Padding ? margin.Padding :
grid.Padding;
return consolidatedGrid;
}

What we are seeing here is that TypeScript works hard to ensure that it can produce code that works regardless of which version of ECMAScript we are targeting. We didn't have to worry whether the feature was available or not; we left it to TypeScript to fill in the blanks for us.

Deconstructing objects with REST properties

Where we used spread operators to build up an object, we can also deconstruct objects with something called a REST property. Deconstructing simply means that we are going to take a complex thing and break it down into simpler ones. In other words, destructuring happens when we assign the elements inside an array or an object's properties to individual variables. While we have always been able to break complex objects and arrays down into simpler types, TypeScript provides a clean and elegant way to break these types down using REST parameters, which can deconstruct both objects and arrays.

In order to understand what REST properties are, we first need to understand how to deconstruct an object or an array. We are going to start off by deconstructing the following object literal, as follows:

let guitar = { manufacturer: 'Ibanez', type : 'Jem 777', strings : 6 };

One way that we could deconstruct this is by using the following:

const manufacturer = guitar.manufacturer;
const type = guitar.type;
const strings = guitar.strings;

While this works, it's not very elegant and there's a lot of repetition. Fortunately, TypeScript adopts the JavaScript syntax for a simple deconstruction like this, which provides a much neater syntax:

let {manufacturer, type, strings} = guitar;

Functionally, this results in the same individual items as the original implementation. The name of the individual properties must match the names of the properties in the object we are deconstructing—that's how the language knows which variable matches with which property on the object. If we need to change the name of the property for some reason, we use the following syntax:

let {manufacturer : maker, type, strings} = guitar;

The idea behind a REST operator on an object is that it applies when you take a variable number of items, so we are going to deconstruct this object into the manufacturer and the other fields are going to be bundled into a REST variable, as follows:

let { manufacturer, ...details } = guitar;
The REST operator must appear at the end of the assignment list; the TypeScript compiler complains if we add any properties after it.

After this statement, details now contains the type and strings values. Where things get interesting is when we look at the JavaScript that has been produced. The form of destructuring in the previous example is the same in JavaScript. There is no equivalent to the REST property in JavaScript (certainly in versions up to ES2018), so TypeScript produces code for us that gives us a consistent way to deconstruct more complex types:

// Compiled as ES5
var manufacturer = guitar.manufacturer, details = __rest(guitar, ["manufacturer"]);
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) &&
e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length;
i++) if (e.indexOf(p[i]) < 0)
t[p[i]] = s[p[i]];
return t;
};

Array destructuring works in a similar fashion to object destructuring. The syntax is virtually identical to the object version; the differences being that it uses [ ] to destructure in place of { }, which the object version uses, and that the order of the variables is based on the position of the item in the array.

The original method of destructuring an array relied on the variable being associated with an item at a certain index in the array:

const instruments = [ 'Guitar', 'Violin', 'Oboe', 'Drums' ];
const gtr = instruments[0];
const violin = instruments[1];
const oboe = instruments[2];
const drums = instruments[3];

Using array destructuring, we can change this syntax to be much more concise, as follows:

let [ gtr, violin, oboe, drums ] = instruments;

Knowing that the TypeScript team are good at providing us with a consistent and logical experience, it should come as no surprise that we can also apply REST properties to arrays, using similar syntax:

let [gtr, ...instrumentslice] = instruments;

Yet again, there is no direct JavaScript equivalent, but the compiled TypeScript shows that JavaScript does provide the underlying fundamentals and the TypeScript designers have been able to elegantly roll this in using array.slice:

// Compiled as ES5
var gtr = instruments[0], instrumentslice = instruments.slice(1);

Coping with a variable number of parameters using REST

The final thing we need to look at with regard to REST is the idea of functions having REST parameters. These aren't the same as REST properties but the syntax is so similar that we should find it easy to pick up. The problem that REST parameters solves is to cope with a variable number of parameters being passed into a function. The way to identify a REST parameter in a function is that it is preceded by the ellipsis and that it is typed as an array.

In this example, we are going to log out a header followed by a variable number of instruments:

function PrintInstruments(log : string, ...instruments : string[]) : void {
console.log(log);
instruments.forEach(instrument => {
console.log(instrument);
});
}
PrintInstruments('Music Shop Inventory', 'Guitar', 'Drums', 'Clarinet', 'Clavinova');

As the REST parameter is an array, this gives us access to array functions, which means that we can perform actions such as forEach from it directly. Importantly, REST parameters are different from the arguments object inside a JavaScript function because they start at the values that have not been named in the parameters list, whereas the arguments object contains a list of all of the arguments.

As REST parameters were not available in ES5, TypeScript does the work necessary to provide JavaScript that simulates the REST parameter. First, we will see what this looks like when compiled as ES5, as follows:

function PrintInstruments(log) {
var instruments = [];
// As our rest parameter starts at the 1st position in the list of
// arguments,
// our index starts at 1.
for (var _i = 1; _i < arguments.length; _i++) {
instruments[_i - 1] = arguments[_i];
}
console.log(log);
instruments.forEach(function (instrument) {
console.log(instrument);
});
}

When we look at the JavaScript produced from an ES2015 compilation (you will need to change the entry for target to ES2015 in the  tsconfig.json file), we see that it looks exactly the same as our TypeScript code:

function PrintInstruments(log, ...instruments) {
console.log(log);
instruments.forEach(instrument => {
console.log(instrument);
});
}

At this point, I cannot stress enough how important it is to take a look at the JavaScript that is being produced. TypeScript is very good at hiding complexity from us, but we really should be familiar with what is being produced. I find it a great way to understand what is going on under the covers, where possible, to compile using different versions of the ECMAScript standard and see what code is being produced.

AOP using decorators

One of my favorite features in TypeScript is the ability to use decorators. Decorators were introduced as an experimental feature and are pieces of code that we can use to modify the behavior of individual classes without having to change the internal implementation of the class. With this concept, we can adapt the behavior of an existing class without having to subclass it.

If you have come to TypeScript from a language such as Java or C#, you might notice that decorators look a lot like a technique known as AOP. What AOP techniques provide us with is the ability to extract repetitive code by cutting across a piece of code and separating this out into a different location. This means that we do not have to litter our implementations with code that will largely be boilerplate code, but which absolutely must be present in the running application.

The easiest way to explain what a decorator is to start off with an example. Suppose we have a class where only users in certain roles can access certain methods, as follows:

interface IDecoratorExample {
AnyoneCanRun(args:string) : void;
AdminOnly(args:string) : void;
}
class NoRoleCheck implements IDecoratorExample {
AnyoneCanRun(args: string): void {
console.log(args);
}
AdminOnly(args: string): void {
console.log(args);
}
}

Now, we are going to create a user who has the admin and user roles, meaning that there are no problems in calling both methods in this class:

let currentUser = {user: "peter", roles : [{role:"user"}, {role:"admin"}] };
function TestDecoratorExample(decoratorMethod : IDecoratorExample) {
console.log(`Current user ${currentUser.user}`);
decoratorMethod.AnyoneCanRun(`Running as user`);
decoratorMethod.AdminOnly(`Running as admin`);
}
TestDecoratorExample(new NoRoleCheck());

This gives us our expected output, as follows:

Current user Peter
Running as user
Running as admin

If we were to create a user who only had the user role, we would expect that they should not be able to run the admin-only code. As our code has no role checking, the AdminOnly method will be run regardless of what roles the user has assigned. One way to fix this code would be to add code to check the entitlement and then add this inside each method.

First, we are going to create a simple function to check whether or not the current user belongs to a particular role:

function IsInRole(role : string) : boolean {
return currentUser.roles.some(r => r.role === role);
}

Revisiting our existing implementation, we are going to change our functions to call this check and determine whether or not user is allowed to run that method:

AnyoneCanRun(args: string): void {
if (!IsInRole("user")) {
console.log(`${currentUser.user} is not in the user role`);
return;
};
console.log(args);
}
AdminOnly(args: string): void {
if (!IsInRole("admin")) {
console.log(`${currentUser.user} is not in the admin role`);
};
console.log(args);
}

When we look at this code, we can see that there is a lot of repeated code in here. Worse still, while we have repeated code, there is a bug in this implementation. In the AdminOnly code, there is no return statement inside the IsInRole block so the code will still run the AdminOnly code, but it will tell us that the user is not in the admin role and will then output the message regardless. This highlights one of the problems with repeated code: it's very easy to introduce subtle (or not-so-subtle) bugs without realizing it. Finally, we are violating one of the basic principles of good object-oriented (OO) development practice. Our classes and methods are doing things that they should not be doing; the code should be doing one thing and one thing only, so checking roles does not belong there. In Chapter 2, Creating a Markdown Editor with TypeScript, we will cover this in more depth when we delve deeper into the OO development mindset.

Let's see how we can use a method decorator to remove the boilerplate code and address the single responsibility issue.

Before we write our code, we need to ensure that TypeScript knows that we are going to use decorators, which are an experimental ES5 feature. We can do this by running the following command from the command line:

tsc --target ES5 --experimentalDecorators

Or, we can set this up in our tsconfig file:

"compilerOptions": {
"target": "ES5",
// other parameters….
"experimentalDecorators": true
}

With the decorator build features enabled, we can now write our first decorator to ensure that a user belongs to the admin role:

function Admin(target: any, propertyKey : string | symbol, descriptor : PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function() {
if (IsInRole(`admin`)) {
originalMethod.apply(this, arguments);
return;
}
console.log(`${currentUser.user} is not in the admin role`);
}
return descriptor;
}

Whenever we see a function definition that looks similar to this, we know that we are looking at a method decorator. TypeScript expects exactly these parameters in this order:

function …(target: any, propertyKey : string | symbol, descriptor : PropertyDescriptor)

The first parameter is used to refer to the element that we are applying it to. The second parameter is the name of the element, and the last parameter is the descriptor of the method we are applying our decorator to; this allows us to alter the behavior of the method. We must have a function with this signature to use as our decorator:

let originalMethod = descriptor.value;
descriptor.value = function() {
...
}
return descriptor;

The internals of the decorator method are not as scary as they look. What we are doing is copying the original method from the descriptor and then replacing that method with our own custom implementation. This wrapped implementation is returned and will be the code that is executed when we encounter it:

if (IsInRole(`admin`)) {
originalMethod.apply(this, arguments);
return;
}
console.log(`${currentUser.user} is not in the admin role`);

In our wrapped implementation, we are performing the same role check. If the check passes, we apply the original method. By using a technique like this, we have added something that will avoid calling our methods if it does not need to in a consistent manner.

In order to apply this, we use @ in front of our decorator factory function name just before the method in our class. When we add our decorator, we must avoid putting a semicolon between it and the method, as follows:

class DecoratedExampleMethodDecoration implements IDecoratorExample {
AnyoneCanRun(args:string) : void {
console.log(args);
}
@Admin
AdminOnly(args:string) : void {
console.log(args);
}
}

While this code works for the AdminOnly code, it is not particularly flexible. As we add more roles, we will end up having to add more and more virtually identical functions. If only we had a way to create a general-purpose function that we could use to return a decorator that would accept a parameter that sets the role we wanted to allow. Fortunately, there is a way that we can do this using something called a decorator factory.

Put simply, a TypeScript decorator factory is a function that can receive parameters and uses the parameters to return the actual decorator. It only needs a couple of minor tweaks to our code and we have a working factory where we can specify the role we want to guard:

function Role(role : string) {
return function(target: any, propertyKey : string | symbol, descriptor
: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function() {
if (IsInRole(role)) {
originalMethod.apply(this, arguments);
return;
}
console.log(`${currentUser.user} is not in the ${role} role`);
}
return descriptor;
}
}

The only real differences here are that we have a function returning our decorator, which no longer has a name, and the factory function parameter is being used inside our decorator. We can now change our class to use this factory instead:

class DecoratedExampleMethodDecoration implements IDecoratorExample {
@Role("user") // Note, no semi-colon
AnyoneCanRun(args:string) : void {
console.log(args);
}
@Role("admin")
AdminOnly(args:string) : void {
console.log(args);
}
}

With this change, when we call our methods, only an admin will be able to access the AdminOnly method, while anyone who is a user will be able to call AnyoneCanRun. An important side note is that, our decorator only applies inside a class. We cannot use this on a standalone function.

The reason we call this technique a decorator is because it follows something called the decorator pattern. This pattern recognizes a technique that is used to add behavior to individual objects without affecting other objects from the same class and without having to create a subclass. A pattern is simply a formalized solution to problems that occur commonly in software engineering, so the names act as a useful shorthand for describing what is going on functionally. It will probably not come as much of a surprise to know that there is also a factory pattern. As we go through this book, we will encounter other examples of patterns, so we will be comfortable using them when we reach the end.

We can apply decorators to other items in a class as well. For instance, if we wanted to prevent an unauthorized user from even instantiating our class, we could define a class decorator. A class decorator is added to the class definition and expects to receive the constructor as a function. This is what our constructor decorator looks like when created from a factory:

function Role(role : string) {
return function(constructor : Function) {
if (!IsInRole (role)) {
throw new Error(`The user is not authorized to access this class`);
}
}
}

When we apply this, we follow the same format of using the @ prefix, so, when the code attempts to create a new instance of this class for a non-admin user, the application will throw an error, preventing this class from being created:

@Role ("admin")
class RestrictedClass {
constructor() {
console.log(`Inside the constructor`);
}
Validate() {
console.log(`Validating`);
}
}

We can see that we have not declared any of our decorators inside a class. We should always create them as a top-level function because their usage is not suited for decorating a class, so we will not see syntax such as @MyClass.Role("admin");.

Beyond constructor and method decorations, we can decorate properties, accessors, and more. We aren't going to go into these here, but they will be cropping up later on in this book. We will also be looking at how we can chain decorators together so we have a syntax that looks as follows:

@Role ("admin")
@Log(“Creating RestrictedClass”)
class RestrictedClass {
constructor() {
console.log(`Inside the constructor`);
}
Validate() {
console.log(`Validating`);
}
}

Composing types using mixins

When we first encounter classic OO theory, we come across the idea that classes can be inherited. The idea here is that we can create even more specialized classes from general-purpose classes. One of the more popular examples of this is that we have a vehicle class that contains basic details about a vehicle. We inherit from the vehicle class to make a car class. We then inherit from the car class to make a sports car class. Each layer of inheritance here adds features that aren't present in the class we are inheriting from. 

In general, this is a simple concept for us to work with, but what happens when we want to bring two or more seemingly unrelated things together to make our code? Let's examine a simple example.

It is a common thing with database applications to store whether a record has been deleted without actually deleting the record, and the time that the last update occurred on the record. At first glance, it would seem that we would want to track this information in a person's data entity. Rather than adding this information into every data entity, we might end up creating a base class that includes this information and then inheriting from it:

class ActiveRecord {
Deleted = false;
}
class Person extends ActiveRecord {
constructor(firstName : string, lastName : string) {
this.FirstName = firstName;
this.LastName = lastName;
}

FirstName : string;
LastName : string;
}

The first problem with this approach is that it mixes details about the status of a record with the actual record itself. As we continue further into OO designs over the next few chapters, we will keep reinforcing the idea that mixing items together like this is not a good idea because we are creating classes that have to do more than one thing, which can make them less robust. The other problem with this approach is that, if we wanted to add the date the record was updated, we are either going to have to add the updated date to ActiveRecord, which means that every class that extends ActiveRecord will also get the updated date, or we are going to have to create a new class that adds the updated date and add this into our hierarchy chain, which means that we could not have an updated field without a deleted field.

While inheritance definitely does have its place, recent years have seen the idea of composing objects together to make new objects gain in prominence. The idea behind this approach is that we build discrete elements that do not rely on inheritance chains. If we revisit our person implementation, we will build the same features using a feature called a mixin instead.

The first thing we need to do is define a type that will act as a suitable constructor for our mixin. We could name this type anything, but the convention that has evolved around mixins in TypeScript is to use the following type:

type Constructor<T ={}> = new(...args: any[]) => T;

This type definition gives us something that we can extend to create our specialized mixins. The strange-looking syntax effectively says that, given any particular type, a new instance will be created using any appropriate arguments.

Here is our record status implementation:

function RecordStatus<T extends Constructor>(base : T) {
return class extends base {
Deleted : boolean = false;
}
}

The RecordStatus function extends the Constructor type by returning a new class that extends the constructor implementation. In this, we add our Deleted flag.

To merge or mix in these two types, we simply do the following:

const ActivePerson = RecordStatus(Person);

This has created something we can use to create a Person object with RecordStatus properties. It has not actually instantiated any objects yet. To do that, we instantiate the information in the same way we would with any other type:

let activePerson = new ActivePerson("Peter", "O'Hanlon");
activePerson.Deleted = true;

Now, we also want to add details about when the record was last updated. We create another mixin, as follows:

function Timestamp<T extends Constructor>(base : T) {
return class extends base {
Updated : Date = new Date();
}
}

To add this to ActivePerson, we change the definition to include Timestamp. It does not matter which mixin we put first, whether it is Timestamp or RecordStatus:

const ActivePerson = RecordStatus(Timestamp(Person));

As well as properties, we can also add constructors and methods to our mixins. We are going to change our RecordStatus function to log out when the record was deleted. To do this, we are going to convert our Deleted property into a getter method and add a new method to actually perform the deletion:

function RecordStatus<T extends Constructor>(base : T) {
return class extends base {
private deleted : boolean = false;
get Deleted() : boolean {
return this.deleted;
}
Delete() : void {
this.deleted = true;
console.log(`The record has been marked as deleted.`);
}
}
}

A word of warning about using mixins like this. They are a great technique, and they provide the ability to neatly do some really useful things, but we cannot pass them as a parameter unless we relax the parameter restrictions to any. That means we cannot use code like this:

function DeletePerson(person : ActivePerson) {
person.Delete();
}
If we look at mixins in the TypeScript documentation at https://www.typescriptlang.org/docs/handbook/mixins.html, we see that the syntax looks very different. Rather than dealing with that approach, with all of the inherent limitations it has, we will stick with the method here, which I was first introduced to at https://basarat.gitbooks.io/typescript/docs/types/mixins.html.

Using the same code with different types and using generics

When we first start developing classes in TypeScript, it is very common for us to repeat the same code again and again, only changing the type that we are relying on. For instance, if we wanted to store a queue of integers, we might be tempted to write the following class:

class QueueOfInt {
private queue : number[]= [];

public Push(value : number) : void {
this.queue.push(value);
}

public Pop() : number | undefined {
return this.queue.shift();
}
}

Calling this code is as easy as this:

const intQueue : QueueOfInt = new QueueOfInt();
intQueue.Push(10);
intQueue.Push(35);
console.log(intQueue.Pop()); // Prints 10
console.log(intQueue.Pop()); // Prints 35

Later on, we decide that we also need to create a queue of strings, so we add code to do this as well:

class QueueOfString {
private queue : string[]= [];

public Push(value : string) : void {
this.queue.push(value);
}

public Pop() : string | undefined {
return this.queue.shift();
}
}

It is easy to see that the more code we add like this, the more tedious our job becomes and the more error-prone. Suppose that we forgot to put the shift operation in one of these implementations. The shift operation allows us to remove the first element from the array and return it, which gives us the core behavior of a queue (a queue operates as First In First Out (or FIFO)). If we had forgotten the shift operation, we would have implemented a stack operation instead (Last In First Out (or LIFO)). This could lead to subtle and dangerous bugs in our code.

With generics, TypeScript provides us with the ability to create something called a generic, which is a type that uses a placeholder to denote what the type is that is being used. It is the responsibility of the code calling that generic to determine what type they are accepting. We recognize generics because they appear after the class name inside <>, or after things such as method names. If we rewrite our queue to use a generic, we will see what this means:

class Queue<T> {
private queue : T[]= [];

public Push(value : T) : void {
this.queue.push(value);
}

public Pop() : T | undefined {
return this.queue.shift();
}
}

Let's break this down:

class Queue<T> {
}

Here, we are creating a class called Queue that accepts any type. The <T> syntax tells TypeScript that, whenever it sees T inside this class, it refers to the type that is passed in:

private queue : T[]= [];

Here is our first instance of the generic type appearing. Rather than the array being fixed to a particular type, the compiler will use the generic type to create the array:

public Push(value : T) : void {
this.queue.push(value);
}

public Pop() : T | undefined {
return this.queue.shift();
}

Again, we have replaced the specific type in our code with the generic instead. Note that TypeScript is happy to use this with the undefined keyword in the Pop method. 

Changing the way we use our code, we can now just tell our Queue object what type we want to apply to it:

const queue : Queue<number> = new Queue<number>();
const stringQueue : Queue<string> = new Queue<string>();
queue.Push(10);
queue.Push(35);
console.log(queue.Pop());
console.log(queue.Pop());
stringQueue.Push(`Hello`);
stringQueue.Push(`Generics`);
console.log(stringQueue.Pop());
console.log(stringQueue.Pop());

What is particularly helpful is that TypeScript enforces the type that we assign wherever it is referenced, so if we attempted to add a string to our queue variable, TypeScript would fail to compile this.

While TypeScript does its best to protect us, we have to remember that it converts into JavaScript. This means that it cannot protect our code from being abused, so, while TypeScript enforces the type we assign, if we were to write external JavaScript that also called our generic types, there is nothing there to prevent adding an unsupported value. The generic is enforced at compile time only so, if we have code that is going to be called from outside our control, we should take steps to guard against incompatible types in our code.

We aren't limited to just having one type in the generic list. Generics allow us to specify any number of types in the definition as long as they have unique names, as follows:

function KeyValuePair<TKey, TValue>(key : TKey, value : TValue)
Keen-eyed readers will note that we have already encountered generics. When we created a mixin, we were using generics in our Constructor type.

What happens if we want to call a particular method from our generic? As TypeScript expects to know what the underlying implementation of the type is, it is strict about what we can do. This means that the following code is not acceptable:

interface IStream {
ReadStream() : Int8Array; // Array of bytes
}
class Data<T> {
ReadStream(stream : T) {
let output = stream.ReadStream();
console.log(output.byteLength);
}
}

As TypeScript cannot guess that we want to use the IStream interface here, it is going to complain if we try to compile this. Fortunately, we can use a generic constraint to tell TypeScript that we have a particular type that we want to use here:

class Data<T extends IStream> {
ReadStream(stream : T) {
let output = stream.ReadStream();
console.log(output.byteLength);
}
}

The <T extends IStream> part tells TypeScript that we are going to use any class that is based on our IStream interface.

While we can constrain generics to types, we are generally going to want to constrain our generics to interfaces. This gives us a lot of flexibility in the classes that we use in the constraint and does not impose limitations that we can only use classes that inherit from a particular base class. 

To see this in action, we are going to create two classes that implement IStream:

class WebStream implements IStream {
ReadStream(): Int8Array {
let array : Int8Array = new Int8Array(8);
for (let index : number = 0; index < array.length; index++){
array[index] = index + 3;
}
return array;
}
}
class DiskStream implements IStream {
ReadStream(): Int8Array {
let array : Int8Array = new Int8Array(20);
for (let index : number = 0; index < array.length; index++){
array[index] = index + 3;
}
return array;
}
}

These can now be used as type constraints in our generic Data implementation:

const webStream = new Data<WebStream>();
const diskStream = new Data<DiskStream>();

We have just told webStream and diskStream that they are going to have access to our classes. To use them, we would still have to pass an instance, as follows:

webStream.ReadStream(new WebStream());
diskStream.ReadStream(new DiskStream());

While we declared our generic and its constraints at the class level, we don't have to do that. We can declare finer-grained generics, down to the method level, if we need to. In this case though, it makes sense to make it a class-level generic if we want to refer to that generic type in multiple places in our code. If the only place we wanted to apply a particular generic was at one or two methods, we could change our class signature to this:

class Data {
ReadStream<T extends IStream>(stream : T) {
let output = stream.ReadStream();
console.log(output.byteLength);
}
}

Mapping values using maps

A situation that often comes up is needing to store a number of items with an easily looked up key. For instance, suppose we had a music collection broken down into a number of genres:

enum Genre {
Rock,
CountryAndWestern,
Classical,
Pop,
HeavyMetal
}

Against each one of these genres, we are going to store the details of a number of artists or composers. One approach we could take would be to create a class that represents each genre. While we could do that, it would be a waste of our coding time. The way we are going to solve this problem is by using something called a map. A map is a generic class that takes in two types: the type of key to use for the map and the type of objects to store in it.

The key is a unique value that is used to allow us to store values or to quickly look things up—this makes maps a good choice for rapidly looking values up. We can have any type as a key and the value can be absolutely anything. For our music collection, we are going to create a class that uses a map with the genre as the key and a string array to represent the composer or artists:

class MusicCollection {
private readonly collection : Map<Genre, string[]>;
constructor() {
this.collection = new Map<Genre, string[]>();
}
}

In order to populate a map, we call the set method, as follows:

public Add(genre : Genre, artist : string[]) : void {
this.collection.set(genre, artist);
}

Retrieving the values from the map is as simple as calling Get with the relevant key:

public Get(genre : Genre) : string[] | undefined {
return this.collection.get(genre);
}
We have to add the undefined keyword to the return value here because there is a possibility that the map entry does not exist. If we forgot to take the possibility of undefined into account, TypeScript helpfully warns us of this. Yet again, TypeScript works hard to provide that robust safety net for our code.

We can now populate our collection, as follows:

let collection = new MusicCollection();
collection.Add(Genre.Classical, [`Debussy`, `Bach`, `Elgar`, `Beethoven`]);
collection.Add(Genre.CountryAndWestern, [`Dolly Parton`, `Toby Keith`, `Willie Nelson`]);
collection.Add(Genre.HeavyMetal, [`Tygers of Pan Tang`, `Saxon`, `Doro`]);
collection.Add(Genre.Pop, [`Michael Jackson`, `Abba`, `The Spice Girls`]);
collection.Add(Genre.Rock, [`Deep Purple`, `Led Zeppelin`, `The Dixie Dregs`]);

If we want to add a single artist, our code becomes slightly more complex. Using set, we either add a new entry into our map or we replace the previous entry with our new one. As this is the case, we really need to check to see whether we have already added that particular key. To do this, we call the has method. If we have not added the genre, we are going to call set with an empty array. Finally, we are going to get the array out of our map using get so that we can push our values in:

public AddArtist(genre: Genre, artist : string) : void {
if (!this.collection.has(genre)) {
this.collection.set(genre, []);
}
let artists = this.collection.get(genre);
if (artists) {
artists.push(artist);
}
}

One more thing we are going to do to our code is change the Add method. Right now, that implementation overwrites previous calls to Add for a particular genre, which means that calling AddArtist and then Add would end up overwriting the artist we added individually with the ones from the Add call:

collection.AddArtist(Genre.HeavyMetal, `Iron Maiden`);
// At this point, HeavyMetal just contains Iron Maiden
collection.Add(Genre.HeavyMetal, [`Tygers of Pan Tang`, `Saxon`, `Doro`]);
// Now HeavyMetal just contains Tygers of Pan Tang, Saxon and Doro

In order to fix the Add method, it is a simple change to iterate over our artists and call the AddArtist method, as follows:

public Add(genre : Genre, artist : string[]) : void {
for (let individual of artist) {
this.AddArtist(genre, individual);
}
}

Now, when we finish populating the HeavyMetal genre, our artists consist of Iron Maiden, Tygers of Pan Tang, Saxon, and Doro.

Creating asynchronous code with promises and async/await

We often need to write code that behaves in an asynchronous fashion. By this, we mean that we need to start a task off and leave it running in the background while we do something else. An example of this could be when we have made a call out to a web service, which may take a while to return. For a long time, the standard way in JavaScript was to use a callback. A big problem with this approach is that the more callbacks we need, the more complex and potentially error-prone our code becomes. This is where promises come in.

A promise tells us that something will happen asynchronously; after the asynchronous operation finishes, we have the option to continue processing and work with the result of the promise, or to catch any exceptions that have been thrown by the exception.

Here's a sample that demonstrates this in action:

function ExpensiveWebCall(time : number) : Promise<void> {
return new Promise((resolve, reject) => setTimeout(resolve, time));
}
class MyWebService {
CallExpensiveWebOperation() : void {
ExpensiveWebCall(4000).then(()=> console.log(`Finished web
service`))
.catch(()=> console.log(`Expensive web call failure`));
}
}

When we write a promise, we optionally take in two parameters—a resolve function and a reject function that can be called to trigger the error handling. Promises supply two functions for us to cope with these values, so then() will be triggered by successfully completing the operation and a separate catch function that copes with the reject function.

Now, we are going to run this code to see its effect:

console.log(`calling service`);
new MyWebService().CallExpensiveWebOperation();
console.log(`Processing continues until the web service returns`);

When we run this code, we get the following output:

calling service
Processing continues until the web service returns
Finished web service

Between the Processing continues until the web service returns and Finished web service lines, there is a four-second delay that we would expect because the application is waiting for the promise to return before it writes out the text in the then() function. What this is demonstrating to us is that the code is behaving asynchronously here because it is not waiting for the web service call to come back when it executed the processing console log.

We might be tempted to think that this code is a bit too verbose, and that scattering Promise<void> is not the most intuitive way for others to understand that our code is asynchronous. TypeScript provides a syntactic equivalent that makes it much more apparent where our code is asynchronous. With the use of the async and await keywords, we easily turn our previous sample into something much more elegant:

function ExpensiveWebCall(time : number) {
return new Promise((resolve, reject) => setTimeout(resolve, time));
}
class MyWebService {
async CallExpensiveWebOperation() {
await ExpensiveWebCall(4000);
console.log(`Finished web service`);
}
}

The async keyword tells us that our function is returning Promise. It also tells the compiler that we want to process the function differently. Where we find await inside an async function, the application will pause that function at that point until the operation that is being awaited returns. At that point, processing continues, mimicking the behavior we saw inside the then() function from Promise.

In order to catch errors in async/await, we really should wrap the code inside the function in a try...catch block. Where the error was explicitly caught by the catch() function, async/await does not have an equivalent way of handling errors, so it is up to us to deal with problems:

class MyWebService {
async CallExpensiveWebOperation() {
try {
await ExpensiveWebCall(4000);
console.log(`Finished web service`);
} catch (error) {
console.log(`Caught ${error}`);
}
}
}
Whichever approach you choose to take is going to be a personal choice. The use of async/await just means it wraps the Promise approach so the runtime behavior of the different techniques is exactly the same. What I do recommend though is, once you decide on an approach in an application, be consistent. Don't mix styles as that will make it much harder for anyone reviewing your application.

Creating UIs with Bootstrap

In the remaining chapters, we are going to be doing a lot of work in the browser. Creating an attractive UI can be a difficult thing to do, especially in an era when we may also be targeting mobile devices in different layout modes. In order to make things easier for ourselves, we are going to rely quite heavily on Bootstrap. Bootstrap was designed to be a mobile device first UI framework that smoothly scales up to PC browsers. In this section, we are going to lay out the base template that contains the standard Bootstrap elements, and then have a look at how to lay out a simple page using features such as the Bootstrap grid system.

We are going to start with the starter template from Bootstrap (https://getbootstrap.com/docs/4.1/getting-started/introduction/#starter-template). With this particular template, we avoid the need to download and install the various CSS stylesheets and JavaScript files; instead, we rely on well-known Content Delivery Networks (CDNs) to source these files for us. 

Where possible, I would recommend using CDNs to source external JavaScript and CSS files. This provides many benefits including not needing to maintain these files ourselves and getting the benefit of browser caching when the browser has encountered this CDN file elsewhere.

The starter template looks as follows:

<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta name="viewport" content="width=device-width, initial-scale=1,
shrink-to-fit=no">
<link rel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap
/4.1.3/css/bootstrap.min.css" integrity="sha384-
MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
<title>
<
<Template Bootstrap>
>
</title>
</head>
<body>
<!--
Content goes here...
Start with the container.
-->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
</html>

The starting point for laying out content is the container. This goes in the preceding content section. The following code shows the div section:

<div class="container">

</div>
The container class gives us that familiar Twitter look where it has a fixed size for each screen size. If we need to fill the full window, we can change this to container-fluid.

Inside the container, Bootstrap attempts to lay items out in a grid pattern. Bootstrap operates a system where each row of the screen can be represented as up to 12 discrete columns. By default, these columns are evenly spread out across the page so we can make complicated layouts just by choosing the appropriate number of columns to occupy for each part of our UI. Fortunately for us, Bootstrap provides an extensive set of predefined styles that help us to make layouts for different types of devices, whether they are PCs, mobile phones, or tablets. These styles all follow the same naming convention of .col-<<size-identifier>>-<<number-of-columns>>:

Type Extra small devices Small devices Medium devices Large devices
Dimensions Phones < 768px Tablets >= 768px Desktops >= 992px Desktops >= 1200px
Prefix .col-xs- .col-sm- .col-md- .col-lg-

 

The way that the number of columns works is that each row should ideally add up to 12 columns. So, if we wanted to have a row made of content covering three columns, then six columns, and finally another three columns, we would define our rows to look like this inside our container:

<div class="row">
<div class="col-sm-3">Hello</div>
<div class="col-sm-6">Hello</div>
<div class="col-sm-3">Hello</div>
</div>

That styling defines how this would appear on small devices. It is possible to override the styles for larger devices. For instance, if we wanted large devices to use columns of five, two, and five, we could apply this styling:

<div class="row">
<div class="col-sm-3 col-lg-5">Hello</div>
<div class="col-sm-6 col-lg-2">Hello</div>
<div class="col-sm-3 col-lg-5">Hello</div>
</div>

This is the beauty of a responsive layout system. It allows us to generate content that is appropriate for our devices.

Let's take a look at how to add some content to our page. We are going to add jumbotron to our first column, some text into our second column, and a button in our third column:

<div class="row">
<div class="col-md-3">
<div class="jumbotron">
<h2>
Hello, world!
</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
eget mi odio. Praesent a neque sed purus sodales interdum. In augue sapien,
molestie id lacus eleifend...
</p>
<p>
<a class="btn btn-primary btn-large" href="#">Learn more</a>
</p>
</div>
</div>
<div class="col-md-6">
<h2>
Heading
</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
eget mi odio. Praesent a neque sed purus sodales interdum. In augue sapien,
molestie id lacus eleifend...
</p>
<p>
<a class="btn" href="#">View details</a>
</p>
</div>
<div class="col-md-3">
<button type="button" class="btn btn-primary btn-lg btn-block active">
Button
</button>
</div>
</div>

Again, we are using CSS styling to control what our display looks like. By giving a div section a styling of jumbotron, Bootstrap immediately applies that styling for us. We controlled exactly what our button looks like by choosing to make it the primary button (btn-primary) and so on. 

jumbotron normally stretches across the width of all of the columns. We put it inside a three-column div just so we can see that the width and styling is controlled by the grid layout system and that jumbotron does not have some special properties that force it to lay out across the page.

When I want to rapidly prototype a layout, I always follow a two-stage process. The first step is to draw on a piece of paper what I want my UI to look like. I could do this using a wireframe tool but I like the ability to quickly draw things out. Once I have got a general idea of what I want my layout to look like, I use a tool such as Layoutit! (https://www.layoutit.com/) to put the ideas on to the screen; this also gives me the option to export the layout so that I can further refine it by hand.
 

Summary

In this chapter, we had a look at features of TypeScript that help us to build future-proof TypeScript code. We looked at how to set the appropriate ES levels to simulate or use modern ECMAScript features. We looked at how to use union and intersection types as well as how to create type aliases. We then looked into object spread and REST properties before we covered AOP with decorators. We also covered how to create and use map types, as well as using generics and promises.

As preparation for the UIs we will be producing in the rest of this book, we briefly looked at using Bootstrap to lay out UIs and covered the basics of the Bootstrap grid layout system.

In the next chapter, we are going to build a simple markdown editor using a simple Bootstrap web page hooked up to our TypeScript. We will see how techniques such as design patterns and single responsibility classes help us to create robust professional code.

 

Questions

  1. We have written an application that allows users to convert from Fahrenheit into Celsius and from Celsius into Fahrenheit. The calculations are performed in the following classes:
class FahrenheitToCelsius {
Convert(temperature : number) : number {
return (temperature - 32) * 5 / 9;
}
}

class CelsiusToFahrenheit {
Convert(temperature : number) : number {
return (temperature * 9/5) + 32;
}
}

We want to write a method that accepts a temperature and an instance of either of these types, which will then perform the relevant calculation. What technique would we use to write this method?

  1. We have written the following class:
class Command {
public constructor(public Name : string = "", public Action : Function = new Function()){}
}

We want to use this in another class where we will add a number of commands. Name of the command is going to be the key that we can use to look up Command later on in our code. What would we use to provide this key-value functionality and how would we add records to it?

  1. How would we automatically log that we were adding entries to the command we added in Question 2 without adding any code inside our Add methods?
  2. We have created a Bootstrap web page where we want to display a row with six medium columns of equal size. How would we do this?

About the Author

  • Peter O'Hanlon

    Peter O’Hanlon has been a professional developer for nearly 30 years. In this time, he has developed desktop and web applications of all types and sizes. During his development career, Peter has worked with languages such as C, C++, BASIC, Pascal, and JavaScript. For the last 18 years or so, Peter has concentrated on C# WPF, JavaScript, and now TypeScript. Over the years, Peter has been a Code Project MVP several times, a member of the Intel Innovator program, as well as competing in the Intel Ultimate Coder 2 contest. Peter is always keen to embrace new technologies and languages, which has led him to be actively involved in technologies such as augmented and mixed reality, computer vision, artificial intelligence, and gesture recognition.

    Browse publications by this author

Latest Reviews

(1 reviews total)
fachlich gut geschrieben und didaktisch gut nachvollziehbar

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial