An introduction to TypeScript + Angular 2

Things I’ve learned with my first Angular 2 Application

Jeff Gensler
7 min readAug 25, 2017

What we’ll learn:

  • How to install and modify the Angular4 starter application
  • Use Swagger to generate Types for our API Backend and fit type/code generation into a workflow.

My experience prior to this application:

  • decent working knowledge of Node.js workflows and tools
  • very basic familiarity with TypeScript (That is uses a type system and requires compilation)
  • frustration from previous web development experience on Angular ~1.3

Setting Up Your Development Environment

First, you’ll want to install nvm by following the instructions here. This will make it easier to manage node/npm versions between your Node.js projects. You will be cognizant of the version of Node.js when developing which will help when adding the project to your CI/CD system.

For my editor, I am using Atom with the atom-typescript plugin. I have found it work pretty well for code completion. I am sure other popular editors will have similar plugins.

Downloading the Sample Project following the instructions here. This guide is helpful for learning TypeScript syntax. If you are like me, it will be frustrating to slow down and read. Trust me, it is worth the wait. You’ll only need to get to the Services section (part 5) to have a good enough grasp to move forward.

Adding Our First Third-Party Package

For our sample application, we will be adding cal-heatmap. Fortunately, there are existing TypeScript types created for us (located here). The typical workflow to install a Third Party Package is the following:

# Ultimately, we need the cal-heatmap sourcecode
npm install --save cal-heatmap
# We also need type mappings for the above javascript
npm install --save-dev @types/cal-heatmap

After installing the type mappings, we need to adjust our build system to understand the newly available code. Usually, we would have to update a tsconfig.json . For whatever reason, anything with @types/* is automatically included at build time (as long as we don’t specify types: []). See this link and the corresponding link for more details about the behavior of type resolution when compiling TypeScript.

Finally, we can update our source code to import the cal-heatmap package. Because we have the following:

<app-root>/node_modules/@types/cal-heatmap

we can use:

# <app-root>/src/app/app.component.ts
import { Component } from '@angular/core';
import { Service, DefaultApi } from './swagger';
import 'cal-heatmap';

An interesting thing to note about the cal-heatmap’sindex.d.ts file is that it does not include any export statements. I believe this means we must import the whole package instead of import { CalHeatMap } from 'cal-heatmap'. If you try an import statement like the one above, you will be met with the following :

ERROR in /private/tmp/angular/my-app/src/app/app.component.ts (3,10): Module '"/private/tmp/angular/my-app/node_modules/@types/cal-heatmap/index"' has no exported member 'CalHeatMap'.

After importing, we can add the following to our App Component:

@Component({
template: "<div id="heatmap"></div>"
})
export class AppComponent {
...
heatmap = new CalHeatMap().init({itemSelctor: "#heatmap"});
}

You’ll get the following error:

VM1978:1626 Uncaught TypeError: Cannot read property 'format' of undefined

Which is caused by the following (d3 is undefined):

formatNumber: d3.format(",g"),

We need to include both d3’s and cal-heatmap’s source code (js + css).

# .angular-cli.json
# I THINK THE ORDER OF JS FILES MATTERS
{
"apps": [
...
"styles": [
"styles.css",
"../node_modules/cal-heatmap/cal-heatmap.css"
],
"scripts": [
"../node_modules/d3/d3.min.js",
"../node_modules/cal-heatmap/cal-heatmap.js"
],
....

However, when we start our application, we can see that the heatmap can’t find the correct element to initialize with. As a way of guaranteeing that the template exists, we can use a (click) event to initialize the heatmap.

@Component({
selector: 'app-root',
template: `
<div><input type="button" (click)="onSelect()">click me</div>
<div id="heatmap"></div>
`
})
export class AppComponent {
onSelect(): void {
this.cal.init({itemSelector: "#heatmap"})
};
cal = new CalHeatMap();
}

Although it is a bit clunky, you should have something like the following:

I still had some code left from the tutorial

Creating our Service and API

With our previous experience with Angular 1.X, we know we need some sort of Service abstraction. We will use this Service to mock the API call that fetches data for our heatmap. We will use swagger-codegen so that we can easily generate a server that will satisfy our API contract.

To start, we can install swagger with node and create an environment to edit our swagger.yaml.

$ npm install -g swagger
...
$ pwd

/tmp/angular
$ ls
my-app
$ swagger project create my-app-backend
...
$ tree -L 2
.
├── my-app
│ ├── README.md
│ ├── e2e
│ ├── karma.conf.js
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── protractor.conf.js
│ ├── src
│ ├── tsconfig.json
│ └── tslint.json
└── my-app-backend
├── README.md
├── api
├── app.js
├── config
├── node_modules
├── package-lock.json
├── package.json
└── test

We can now use swagger project edit to edit swagger.yaml . I’ve added a script to my Angular projects’s project.json to generate the TypeScript files:

"swagger": "swagger-codegen generate -i ../my-app-backend/api/swagger/swagger.yaml -l typescript-angular2 -o ./src/app/swagger"

You’ll need swagger-codegen installed (brew install swagger-codegen).

Injecting a Mock Component at Runtime

At first, I found a bunch of conflicting links on how to inject a mock service at runtime. Most of these posts revolved around complex initialization of modules/components. All we are looking to do is *not* invoke an HTTP request when a function is called. To do this, we know we will need a class that acts like the original class. We need a mock!

I have started with adding a Factory function. This gives us the behavior we want but still lacks a few things.

providers: [
{
provide: DefaultApi,
deps: [Http, Environment],
useFactory: function(http: Http, env: Environment){
if(env.environment().shouldMockApi){
return new MockDefaultApi(http);
} else {
return new DefaultApi(http, "", null);
}
}
}
]

Notably:

  • If other Components need a similar style of mocking, we will duplicate the useFactory block elsewhere. This means that every “provided” Component will need a mock.
  • The useFactory function doesn’t easily support the easy injection behavior that Angular gives us by default. In my case, I don’t know how to get the BASE_PATH or Configuration that the DefaultApi class needs. To me, the above solution “feels wrong” because we are doing something that Angular is already trying to do for us. See this comment for more detail.
  • If we follow how Dependency Injection usually plays out, we ought to be modifying a dependency of DefaultApi . I think this means we would always provide the same DefaultApi Component with mockedHttp Component.
    One argument for keeping the useFactory function is that there may be many components that need their own uniquely mocked Http Components.

After a bit of searching, I have found this link. Instead of using the useFactory function, we can use the useClass function. However, we can define this behavior application-wide rather than in the Component itself. This keeps our Component files nice and tidy and push configuration related behavior to a more appropriate place.

Using a Mock API Layer

At this point, I’ve noticed that there isn’t an easy way to regenerate the server code from swagger project create . Instead, we can use the same swagger-codegen script from before but substitute nodejs-server for angular-typescript2.

When you are have generated and run your generated server, you’ll notice that the /alerts endpoint doesn’t return anything. To solve this, we can add “Examples” to our swagger.yaml file and our templated Server code should automatically pick this up. There are several places to provide examples: request body, response body, and in the schema definition. You can even define an external example! For our use case, we will define one example for every schema.

Note: be careful to edit the correct swagger.yaml file! Your server code will contain one as well!

We can copy our existing example from here. One interesting behavior I have found regarding example data is an endpoint returning an Array of a particular object (ex:/alerts). There are two options for defining the response body of this endpoint:

# Option 1
responses:
200:
description: OK
schema:
type: array
items:
$ref: '#/definitions/Alert'
# -- or --
# Option 2
responses:
200:
description: OK
schema:
$ref: '#/definitions/AlertList'
# Where AlertList is responses.200.schema

With Option 2, an AlertList doesn’t get generated from “Example” data in the Alert schema. Summing up, only create schemas for singular objects and not Array-abstraction objects (and choose Option 1).

Option 1: Example data only defined in Alert’s schema

If we want to move a step up from Swagger examples, we can use swagger-poser/swagger-faker/swagger-gen to generate arbitrarily large amounts of data.

Combining the Backend and Frontend

It will be useful if the two components live separately. One development team might own the frontend and one team might own that backend. Also, we may deliver our static content via a CDN whereas our backend will be delivered by a Docker container.

Analyzing how to test the two components locally, we have the following:

npm run-script start
<server listening on localhost:8080>
ng serve --environment=prod
<server listening on localhost:4200>

This definitely looks like it will be a CORS issue. To solve this, we can front both of the servers with NGINX. We will proxy anything that looks like /api/... to the backend service and every other request will be sent to the angular server. Make sure you update your swagger.yaml to have a basePath: /api .

http {
upstream swagger {
server 192.168.1.29:8080;
}
upstream ngserve {
server 192.168.1.29:4200;
}
server {
listen 80;
location ^~ /api {
proxy_pass http://swagger;
}
location / {
proxy_pass http://ngserve;
}
}
}

Then we can build and run it!

$ npm start
$ ng serve --host 0.0.0.0 --disable-host-check --environment prod
$ docker run -it --name cors --rm -p 80:80 cors
Because the Swagger Example has a predefined time, it falls outside of our range of Now +/- 2 days.

Unfortunately, we can’t use the same class-like “extends” of our backend code. I think the best case would be to implement a new Controllers directory and modify index.js to read from either directory based on the NODE_ENV or a similar variable.

--

--