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.

Tweet This Interesting post by @BenNadel - Better Type Checking With In-Browser TypeScript Transpiling In Angular 2 Woot woot — you rock the party that rocks the body!







