Better Type Checking With In-Browser TypeScript Transpiling In Angular 2
A couple of days ago, I posted that I was going to start writing my Angular 2 demos using System.js and TypeScript. And, while I got something working, based on the Getting Started guide for Angular 2, it didn't really deliver a large portion of the value-add for TypeScript: type checking. Fortunately, Frank Wallis and Guy Bedford were most excellent enough to help me out and showed me how to enable type checking when using the in-browser TypeScript transpiling. I don't fully understand how all of this blood-magic works yet; but, I can see that type checking is, indeed, happening in the browser.
Run this demo in my JavaScript Demos project on GitHub.
To get type checking working with the in-browser TypeScript transpiling, I had to update my tsconfig.json file to enable the "typeCheck" option:
{
"compilerOptions": {
"target": "es5",
"module": "system",
"moduleResolution": "node",
"sourceMap": true,
"typeCheck": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
"exclude": [
"node_modules",
"typings/main",
"typings/main.d.ts"
]
}
Then, I had to update my system.config.js file to tell System.js where to find the TypeScript definition files for the various *.js modules (RxJS and the Angular 2 modules):
(function() {
// Alias the path to the common rc1 vendor scripts.
var paths = {
"rc1/*": "../../vendor/angularjs-2-beta/rc1/*"
};
// Tell Angular how normalize path and package aliases.
var map = {
"@angular": "rc1/node_modules/@angular",
"plugin-typescript": "rc1/node_modules/plugin-typescript/lib/plugin.js",
"rxjs": "rc1/node_modules/rxjs",
"tsconfig.json": "rc1/tsconfig.json",
"typescript": "rc1/node_modules/typescript"
};
// Setup meta data for individual areas of the application.
var packages = {
"app": {
main: "main.ts",
defaultExtension: "ts",
meta: {
"*.ts": {
loader: "plugin-typescript"
}
}
},
"rc1/node_modules": {
defaultExtension: "js"
},
"rxjs": {
meta: {
"*.js": {
typings: true
}
}
},
"typescript": {
main: "lib/typescript.js",
meta: {
"lib/typescript.js": {
exports: "ts"
}
}
}
};
var ngPackageNames = [
"common",
"compiler",
"core",
"http",
"platform-browser",
"platform-browser-dynamic",
"router",
"router-deprecated",
"upgrade",
];
ngPackageNames.forEach(
function iterator( packageName ) {
var filename = ( packageName + ".umd.js" );
var ngPackage = packages[ "@angular/" + packageName ] = {
main: filename,
meta: {}
};
ngPackage.meta[ filename ] = {
typings: ( packageName + "/index.d.ts" )
};
}
);
System.config({
paths: paths,
map: map,
packages: packages,
transpiler: "plugin-typescript",
typescriptOptions: {
tsconfig: true
},
meta: {
typescript: {
exports: "ts"
}
}
});
// Load "./app/main.ts" (gets full path from package configuration above).
System
.import( "app" )
.then(
function handleAppResolve() {
// Force type checking for typed candidates.
// --
// CAUTION: I am not sure what this does exactly, and the demo seems to
// work fine without it.
var promise = System
.import( "plugin-typescript" )
.then(
function handlePluginResolve( plugin ) {
return( plugin.bundle() );
}
)
;
return( promise );
}
)
.then(
function handleResolve() {
console.info( "System.js successfully bootstrapped app." );
},
function handleReject( error ) {
console.warn( "System.js could not bootstrap the app." );
console.error( error );
}
)
;
})();
In the System.import() promise chain, you'll notice that I'm explicitly invoking the TypeScript plugin's .bundle() method. At the time of this writing, I don't know exactly what that does. Looking through the plugin source code, it seems that .bundle() forces type-checking to be performed on the syntax tree of the loaded components. But, the truth is, the demo works with or without this portion of the code. That said, I'm leaving it in for the time being since I don't truly understand what it does.
The moment I enabled this, I actually found 2 different type problems with my previous demo (which I outlined in the comments of that post). This goes to show you how important type checking is (if you're going to use it).
Once I had this type checking enabled, I went about updating my demo to use better typing. First, let's look at the FriendService since this is where the data comes from. You'll notice that I've add a Friend interface (for the JSON payload) and I've updated my method signatures to incorporate this type:
// Import the core angular services.
import { Http } from "@angular/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Response } from "@angular/http";
// Enable RxJS operators (importing for SIDE-EFFECTS only).
import "rxjs/add/operator/map";
// I define the shape of the Friend data structure.
export interface Friend {
id: number;
name: string;
isBFF: boolean;
}
// I provide a service for accessing the Friend repository.
@Injectable()
export class FriendService {
// I hold the URL prefix for the API call.
private baseUrl: string;
// I provide an HTTP client implementation.
private http: Http;
// I initialize the service.
constructor( http: Http ) {
this.baseUrl = "./app/";
this.http = http;
}
// ---
// PUBLIC METHODS.
// ---
// I return the entire collection of friends as an Observable.
public getFriends() : Observable<Friend[]> {
var stream = this.http
.get( this.baseUrl + "friends.json" )
.map( this.unwrapResolve )
;
return( stream );
}
// ---
// PRIVATE METHODS.
// ---
// I unwrap the raw HTTP response, returning the deserialized data.
private unwrapResolve( response: Response ) : Friend[] {
return( response.json() );
}
}
Notice that even the Observable signature includes this new Friend type.
Then, I updated my AppComponent to consume these new type annotations:
// Import the core angular services.
import { Component } from "@angular/core";
import { HTTP_PROVIDERS } from "@angular/http";
import { OnInit } from "@angular/core";
// Import the application components and services.
// --
// NOTE: I'm aliasing the Friend interface just to experiment with the syntax.
import { Friend as IFriend } from "./friend.service";
import { FriendService } from "./friend.service";
// I provide the root component of the application.
@Component({
selector: "my-app",
providers: [ FriendService, HTTP_PROVIDERS ],
template:
`
<div *ngIf="isLoading" class="loading">
Loading friends...
</div>
<div *ngIf="isDoneLoading">
<p>
You Have {{ friends.length }} friends!
</p>
<ul>
<li *ngFor="let friend of friends" [class.is-bff]="friend.isBFF">
<span>{{ friend.name }}</span>
</li>
</ul>
</div>
`
})
export class AppComponent implements OnInit {
// I hold the collection of friends to display.
public friends: IFriend[];
// I provide access to the friend repository.
public friendService: FriendService;
// I determine if the data has been fully loaded.
public isDoneLoading: boolean;
public isLoading: boolean;
// I initialize the component.
constructor( friendService: FriendService ) {
this.friends = [];
this.friendService = friendService;
this.isDoneLoading = false;
this.isLoading = true;
}
// ---
// PUBLIC METHODS.
// ---
// I get called once after the component has been instantiated and the input
// properties have been bound.
public ngOnInit(): void {
this.friendService
.getFriends()
.subscribe( handleResolve.bind( this ) )
;
function handleResolve( newFriends: IFriend[] ) : void {
this.friends = newFriends;
// Flag the data as fully loaded.
this.isLoading = false;
this.isDoneLoading = true;
}
}
}
If you run this demo, everything works as expected. But, the beautiful part is that if I go in and start messing with the type annotations, such that types don't line up, the in-browser transpiler and type checker will throw an error. For example, if I go into the FriendService and change the unwrapResolve() method signature from:
private unwrapResolve( response: Response ) : Friend[]
to:
private unwrapResolve( response: Response ) : string[]
... running the demo will result in the following TypeScript error:
TypeScript Type 'Observable<string[]>' is not assignable to type 'Observable<Friend[]>'.
Enabling type checking for the in-browser TypeScript transpiler definitely has overhead. It loads about twice as many JavaScript files and it takes noticeably longer for the page to load (especially in Firefox). But, these are just demos; and for me, learning about TypeScript and proper type usage is more important than a fast page load time.
Want to use code from this post? Check out the license.
Reader Comments
I believe the reason the compiler doesn't complain when setting the wrong type for the properties inside the `handleResolve()` function is that by using `.bind()`, you lose all type information for that function (see [here](https://basarat.gitbooks.io/typescript/content/docs/tips/bind.html)). Try using an arrow function instead.
@Greg,
Oh snap! You totally nailed it. When I removed the .bind(), the type checking starts working. I can even use the old-school `var self = this` approach and then reference `self` instead of `this` inside the function. Of course, as you suggest, using a fat-arrow function works as well. But, there's something very nice (I think) about having the Rx / Promise chain tell the "story" and then have the implementation in separate little functions below. Just a personal preference.
Thanks for the link as well - that looks like a good resource to learn from.
First GREAT to see you moving to TS, once you do, you will never go back, it's awesome!
also, I am wondering what's the advantage of type checking in the browser / runtime?
I do all my development in WebStorm and use type checking during dev, but at runtime it all gets stripped out which is fine... just wondering...
and FYI, I highly recommend the superset project ofr Guy, JSPM, which is SytemJS on Steroids... SOOOO powerful, it's amazing.
https://github.com/jspm/jspm-cli
Angular 2 Kitchen sink: http://ng2.javascriptninja.io
and source@ https://github.com/born2net/ng2Boilerplate
Regards,
Sean
@Sean,
Very good question. Right now, I only use Sublime Text 2 (ST2), which has an old TypeScript plugin which offers little more than syntax highlight - no realtime functionality. So, for my current R&D workflow, the in-browser type checking is literally all I have.
Now, I know that I could probably get all that stuff working (would probably have to upgrade to ST3 to get better performance with the TypeScript plugin) or use the NPM scripts for running the TypeScript compiler in the background. But, the other hope that I have for many of demos is that people can just "git clone" my stuff and it _just works_ (with the exception of the ColdFusion server-side portions).
I guess, bottom line, for my R&D stuff, I'm trying as hard as I can to avoid a build step. But, absolutely, your mileage my vary!
Great question - thanks for challenging my view point. Definitely, this is just for R&D - I would not use this approach for production, of course.
@Sean,
Also, thanks for the links - I'll take a look.
@All,
After Greg pointed out that .bind() problem, I put together a quick exploration of the various ways to get callbacks working without .bind():
www.bennadel.com/blog/3096-maintaining-proper-type-checking-with-callbacks-using-typescript-in-angular-2-rc1.htm
Four approaches, all just personal preference.
@All,
The TypeScript plugin for System.js has dropped support for type-checking, which is , obviously, a huge reason for using TypeScript. As such, I've started to compile my demos offline using the tsc compiler, but can still load them with System.js:
www.bennadel.com/blog/3240-building-javascript-demos-with-system-js-typescript-2-2-1-and-angular-2-4-9.htm
Also, as a cool side-note, you no longer need the moduleId meta-data to load module-relative templates and style URLs if you use this System.js loader / plugin:
www.bennadel.com/blog/3241-relative-template-and-style-urls-using-system-js-without-moduleid-in-angular-2-4-9.htm