
AngularJSをTypeScriptで書くと分かりやすいコードが書ける。
それを実証するために、angular-fullstakをTypeScriptで書き換えてみる。
参考文献
AngularJS+TypeScriptを試してみた。
なお、ここで想定している環境は下記の通り。
・Lubuntu(Ubuntu) 14.04
・angular-fullstakでアプリ構築済み
構築していない場合は下記URLを参考に構築すること
YEOMANを使ってMEAN(MongoDB+Express+AngularJS+Node.js)を作成しよう
・TypeScript開発環境構築済み
構築していない場合は下記URLを参考に構築すること
AtomでTypeScriptの開発環境を構築しよう
1. 型定義ファイルをインストールする
下記コマンドを実行
tsd install angular-ui-router --save tsd install angular-resource --save tsd install angular-cookies --save tsd install lodash --save tsd install socket.io-client --save
※ 他にも必要なファイルがある場合は、適宜インストールすること
2. TypeScriptのコンパイラーターゲットをES6にする
tsconfig.jsonで、targetをes6に変更する。(すでにangular-fullstackがBabelを使ってES6で書かれているため)
{ "compilerOptions": { "target": "es6", } }
次に、angular-fullstakの元々のコードとTypeScriptに書き換えたコードを列挙していく。
3. controllerを書き換える
例として client/app/main/main.controller.js を書き換える。
元のJavaScriptがすでにES6によるclass化がされているので、変換後のJavaScriptも元のJavaScriptとほとんど変わらない。
元のJavaScriptは下記の通り。
'use strict'; (function() { class MainController { constructor($http, $scope, socket) { this.$http = $http; this.awesomeThings = []; $http.get('/api/things').then(response => { this.awesomeThings = response.data; socket.syncUpdates('thing', this.awesomeThings); }); $scope.$on('$destroy', function() { socket.unsyncUpdates('thing'); }); } addThing() { if (this.newThing) { this.$http.post('/api/things', { name: this.newThing }); this.newThing = ''; } } deleteThing(thing) { this.$http.delete('/api/things/' + thing._id); } } angular.module('sampleApp') .controller('MainController', MainController); })();
TypeScriptは下記の通り。
ここでconstructorの引数socketの型がanyであることに注目。
anyというは「何でもあり」を意味し、型を定義していないのと同義なので、TypeScriptにするメリットが得られない。
しかし、socketはangular-fullstackが作成しているので型定義ファイルが無い。
そこでとりあえずはanyとしておき、後でsocketの型を定義することにする。
main.controller.ts
/// <reference path="../../../typings/tsd.d.ts" /> 'use strict'; class MainController { private awesomeThings: {}; private newThing: string; constructor(private $http: angular.IHttpService, $scope: angular.IScope, socket: any) { $http.get('/api/things').then(response => { this.awesomeThings = response.data; socket.syncUpdates('thing', this.awesomeThings); }); $scope.$on('$destroy', function() { socket.unsyncUpdates('thing'); }); } public addThing(): void { if (this.newThing) { this.$http.post('/api/things', { name: this.newThing }); this.newThing = ''; } } public deleteThing(thing): void { this.$http.delete('/api/things/' + thing._id); } } angular.module('sampleApp') .controller('MainController', MainController);
変換後のJavaScriptは下記の通り。
/// <reference path="../../../typings/tsd.d.ts" /> 'use strict'; class MainController { constructor($http, $scope, socket) { this.$http = $http; $http.get('/api/things').then(response => { this.awesomeThings = response.data; socket.syncUpdates('thing', this.awesomeThings); }); $scope.$on('$destroy', function () { socket.unsyncUpdates('thing'); }); } addThing() { if (this.newThing) { this.$http.post('/api/things', { name: this.newThing }); this.newThing = ''; } } deleteThing(thing) { this.$http.delete('/api/things/' + thing._id); } } angular.module('sampleApp') .controller('MainController', MainController);
4. serviceを書き換える
先ほどanyとしたsocketをclass化して型を定義する。
client/components/socket/socket.service.js
元のJavaScirptは下記の通り。
/* global io */ 'use strict'; angular.module('sampleApp') .factory('socket', function(socketFactory) { // socket.io now auto-configures its connection when we ommit a connection url var ioSocket = io('', { // Send auth token on connection, you will need to DI the Auth service above // 'query': 'token=' + Auth.getToken() path: '/socket.io-client' }); var socket = socketFactory({ ioSocket }); return { socket, /** * Register listeners to sync an array with updates on a model * * Takes the array we want to sync, the model name that socket updates are sent from, * and an optional callback function after new items are updated. * * @param {String} modelName * @param {Array} array * @param {Function} cb */ syncUpdates(modelName, array, cb) { cb = cb || angular.noop; /** * Syncs item creation/updates on 'model:save' */ socket.on(modelName + ':save', function (item) { var oldItem = _.find(array, {_id: item._id}); var index = array.indexOf(oldItem); var event = 'created'; // replace oldItem if it exists // otherwise just add item to the collection if (oldItem) { array.splice(index, 1, item); event = 'updated'; } else { array.push(item); } cb(event, item, array); }); /** * Syncs removed items on 'model:remove' */ socket.on(modelName + ':remove', function (item) { var event = 'deleted'; _.remove(array, {_id: item._id}); cb(event, item, array); }); }, /** * Removes listeners for a models updates on the socket * * @param modelName */ unsyncUpdates(modelName) { socket.removeAllListeners(modelName + ':save'); socket.removeAllListeners(modelName + ':remove'); } }; });
Socket.syncUpdatesの引数cbにデフォルト引数が付いていることに注目。
呼び出し側でcbを省略したときはangular.noopが適用される。
実際main.controller.tsではcb未設定で呼び出されている。
試しにcbのデフォルト引数を削除するとmain.controller.tsでSocketのエラーになる。
※ 元のコードではunsyncUpdates()でsocket.removeAllListeners()を使っているが、socket.removeListener()の間違いと思われる。(こういう間違いが見つけやすいのがTypeScriptの良いところだろう)
※ Socket.constructor()の引数socketFactoryがanyとなっているのは、angular-socket-ioの.d.tsファイルが無いため、型定義できないためである。
socket.service.ts
/// <reference path="../../../typings/tsd.d.ts" /> /* global io */ 'use strict'; class Socket { private socket: SocketIOClient.Socket; constructor(socketFactory: any) { // socket.io now auto-configures its connection when we ommit a connection url var ioSocket = io('', { // Send auth token on connection, you will need to DI the Auth service above // 'query': 'token=' + Auth.getToken() path: '/socket.io-client' }); this.socket = socketFactory({ ioSocket }); } /** * Register listeners to sync an array with updates on a model * * Takes the array we want to sync, the model name that socket updates are sent from, * and an optional callback function after new items are updated. * * @param {String} modelName * @param {Array} array * @param {Function} cb */ public syncUpdates(modelName: string, array: any, cb: Function = angular.noop): void { /** * Syncs item creation/updates on 'model:save' */ this.socket.on(modelName + ':save', function(item: any): void { var oldItem = _.find(array, {_id: item._id}); var index = array.indexOf(oldItem); var event = 'created'; // replace oldItem if it exists // otherwise just add item to the collection if (oldItem) { array.splice(index, 1, item); event = 'updated'; } else { array.push(item); } cb(event, item, array); }); /** * Syncs removed items on 'model:remove' */ this.socket.on(modelName + ':remove', function(item: any): void { var event = 'deleted'; _.remove(array, {_id: item._id}); cb(event, item, array); }); }; /** * Removes listeners for a models updates on the socket * * @param modelName */ public unsyncUpdates(modelName: string): void { this.socket.removeListener(modelName + ':save'); this.socket.removeListener(modelName + ':remove'); }; } angular.module('sampleApp') .service('socket', Socket);
変換後のJavaScriptは下記の通り。
/// <reference path="../../../typings/tsd.d.ts" /> /* global io */ 'use strict'; class Socket { constructor(socketFactory) { // socket.io now auto-configures its connection when we ommit a connection url var ioSocket = io('', { // Send auth token on connection, you will need to DI the Auth service above // 'query': 'token=' + Auth.getToken() path: '/socket.io-client' }); this.socket = socketFactory({ ioSocket: ioSocket }); } /** * Register listeners to sync an array with updates on a model * * Takes the array we want to sync, the model name that socket updates are sent from, * and an optional callback function after new items are updated. * * @param {String} modelName * @param {Array} array * @param {Function} cb */ syncUpdates(modelName, array, cb = angular.noop) { /** * Syncs item creation/updates on 'model:save' */ this.socket.on(modelName + ':save', function (item) { var oldItem = _.find(array, { _id: item._id }); var index = array.indexOf(oldItem); var event = 'created'; // replace oldItem if it exists // otherwise just add item to the collection if (oldItem) { array.splice(index, 1, item); event = 'updated'; } else { array.push(item); } cb(event, item, array); }); /** * Syncs removed items on 'model:remove' */ this.socket.on(modelName + ':remove', function (item) { var event = 'deleted'; _.remove(array, { _id: item._id }); cb(event, item, array); }); } /** * Removes listeners for a models updates on the socket * * @param modelName */ unsyncUpdates(modelName) { this.socket.removeListener(modelName + ':save'); this.socket.removeListener(modelName + ':remove'); } } angular.module('sampleApp') .service('socket', Socket);
先ほどのmain.controller.tsでsocketの型を定義する。
/// <reference path="../../../typings/tsd.d.ts" /> /// <reference path="../../components/socket/socket.service.ts" /> 'use strict'; class MainController { private awesomeThings: {}; private newThing: string; constructor(private $http: angular.IHttpService, $scope: angular.IScope, socket: Socket) {
5. directiveを書き換える
client/components/footer/footer.directive.jsを書き換えてみる。
元のJavaScriptは下記の通り。
'use strict'; angular.module('sampleApp') .directive('footer', function () { return { templateUrl: 'components/footer/footer.html', restrict: 'E', link: function(scope, element) { element.addClass('footer'); } }; });
TypeScriptは下記の通り。
footer.directive.ts
/// <reference path="../../../typings/tsd.d.ts" /> 'use strict'; class Footer implements angular.IDirective { templateUrl = 'components/footer/footer.html'; restrict = 'E'; link(scope: angular.IScope, element: angular.IAugmentedJQuery): void { element.addClass('footer'); } } angular.module('sampleApp') .directive('footer', () => new Footer());
変換後のJavaScriptは下記の通り。
/// <reference path="../../../typings/tsd.d.ts" /> 'use strict'; class Footer { constructor() { this.templateUrl = 'components/footer/footer.html'; this.restrict = 'E'; } link(scope, element) { element.addClass('footer'); } } angular.module('sampleApp') .directive('footer', () => new Footer());
6. angular-resourceを書き換える
最後におまけとしてangular-resourceを使っているclient/components/auth/user.service.jsを書き換えてみる。
angular-resourceは元のJavaScriptから随分違うコードに書き換える必要があるので注意が必要だ。
参考文献: How to use AngularJS ng.resource.IResource with TypeScript.
元のJavaScriptは下記の通り。
'use strict'; (function() { function UserResource($resource) { return $resource('/api/users/:id/:controller', { id: '@_id' }, { changePassword: { method: 'PUT', params: { controller:'password' } }, get: { method: 'GET', params: { id:'me' } } }); } angular.module('sampleApp.auth') .factory('User', UserResource); })();
TypeScriptは下記の通り。
user.service.ts
/// <reference path="../../../typings/tsd.d.ts" /> 'use strict'; interface Password { oldPassword: string; newPassword: string; } interface User extends angular.resource.IResource<User> { _id: string; name: string; email: string; password: string; role: string; } interface UserResource extends angular.resource.IResourceClass<User> { changePassword(id: any, password: Password, successCallback: any, errorCallback: any): User; get(): User; } angular.module('sampleApp.auth') .factory('User', ($resource: angular.resource.IResourceService) => { var changePasswordAnction: angular.resource.IActionDescriptor = { method: 'PUT', params: { controller: 'password' } }; var getAction: angular.resource.IActionDescriptor = { method: 'GET', params: { id: 'me' } }; return <UserResource>$resource('/api/users/:id/:controller', { id: '@_id' }, { changePassword: changePasswordAnction, get: getAction }); } );
変換後のJavaScriptは下記の通り。
/// <reference path="../../../typings/tsd.d.ts" /> 'use strict'; angular.module('sampleApp.atuh') .factory('User', ($resource) => { var changePasswordAnction = { method: 'PUT', params: { controller: 'password' } }; var getAction = { method: 'GET', params: { id: 'me' } }; return $resource('/api/users/:id/:controller', { id: '@_id' }, { changePassword: changePasswordAnction, get: getAction }); });