今まで書いたサンプル程度の規模でも、 実行時に踏むまでバグに気付かないことが結構あって、 実際踏むと萎える。 React で規模の大きい SPA 作っていたらと思うとゾッとする。 コンパイル時に分かれば良いのに。
そうだ。 TypeScript で書いてみよう。 JavaScript のスーパーセットというアプローチが、 AltJS の中でも筋が良いと思っていたんだよな。
TypeScript と、型定義ファイルを管理するための tsd をインストール。
npm install -g typescript npm install -g tsd
React と React-DOM、 その他使っているライブラリの型定義ファイルをインストールする。
cd src tsd query react --action install --resolve tsd query react-dom --action install --resolve tsd query lodash --action install --resolve
TypeScript コードを JavaScript にコンパイルするには、tsc を使う。 毎回オプションを指定するのは面倒なので、 tsconfig.json を書いておく。
{ "compilerOptions": { "module": "commonjs", "jsx": "react", "rootDir": "src", "outDir": "built" }, "exclude": [ "node_modules" ] }
これで tsc
を実行するだけでコンパイルできる。
ただ、tsc で JavaScript にコンパイルできるけど、
これをブラウザで実行できるようにしないといけない。
TypeScript になっても Browserify のお世話になるみたいだ。
package.json の scripts に書いている build コマンドを修正。
"scripts": { "build": "tsc && browserify built/app.js --outfile dist/bundle.js" },
これでようやく準備が終わった。 試しに、TODO アプリのサンプルを TypeScript で書き換えてみた。
/// <reference path="typings/tsd.d.ts" /> import * as React from "react" import * as ReactDOM from "react-dom" import * as _ from "lodash" // タスクのインタフェース interface ITask { name?: string; completed?: boolean; editing?: boolean; } // タスクを保存するときに呼び出すコールバックのインタフェース interface ITaskSaveCallback { (task: ITask, newValue: ITask) : void; } // タスクを削除するときに呼び出すコールバックのインタフェース interface ITaskDeleteCallback { (task: ITask) : void; } // タスクを表示するコンポーネントの props のインタフェース interface ITaskProps { task: ITask; onTaskSave: ITaskSaveCallback; onTaskDelete: ITaskDeleteCallback; } // タスクを表示するコンポーネント class Task extends React.Component<ITaskProps, any> { private handleEdit = (e) => { this.props.onTaskSave(this.props.task, { editing: true }); } private handleDelete = (e) => { this.props.onTaskDelete(this.props.task); } private handleChangeCheckbox = (e) => { this.props.onTaskSave(this.props.task, { completed: !this.props.task.completed }); } render(): JSX.Element { return ( <div> <input type="checkbox" checked={this.props.task.completed} onChange={this.handleChangeCheckbox} /> <span>{this.props.task.name}</span> <a href="#" onClick={this.handleEdit}>[edit]</a> <a href="#" onClick={this.handleDelete}>[del]</a> </div> ); } } // タスクを編集するコンポーネントの props のインタフェース interface IEditTaskFormProps { task: ITask; onTaskSave: ITaskSaveCallback; } // タスクを編集するコンポーネントの state のインタフェース interface IEditTaskFormState { name: string; } // タスクをその場編集するためのコンポーネント class EditTaskForm extends React.Component<IEditTaskFormProps, IEditTaskFormState> { constructor(props: IEditTaskFormProps) { super(props); this.state = { name: "" }; } private handleCancel = (e) => { this.props.onTaskSave(this.props.task, { editing: false }); } private handleSave = (e) => { e.preventDefault(); if (!this.state.name) { return; } this.props.onTaskSave(this.props.task, { name: this.state.name, editing: false }); this.setState({ name: "" }); } private handleChangeText = (e) => { this.setState({ name: e.target.value }); } render(): JSX.Element { return ( <div> <input type="text" value={this.state.name || this.props.task.name} onChange={this.handleChangeText} ref="name"/> <button onClick={this.handleSave}>save</button> <button onClick={this.handleCancel}>cancel</button> </div> ); } } // タスク一覧を表示するコンポーネントの props のインタフェース interface ITaskListProps { tasks: ITask[]; onTaskSave: ITaskSaveCallback; onTaskDelete: ITaskDeleteCallback; } // タスク一覧を表示するためのコンポーネント class TaskList extends React.Component<ITaskListProps, any> { render(): JSX.Element { var rows = this.props.tasks.map(t => { if (t.editing) { return ( <EditTaskForm task={t} onTaskSave={this.props.onTaskSave} /> ); } else { return ( <Task task={t} onTaskSave={this.props.onTaskSave} onTaskDelete={this.props.onTaskDelete} /> ); } }); return ( <div> {rows} </div> ); } } // タスクを追加するときに呼び出すコールバックのインタフェース interface ITaskAddCallback { (task: ITask): void; } // タスク追加フォームの props のインタフェース interface IAddTaskFormProps { onTaskAdd: ITaskAddCallback; } // タスク登録フォームを表示するためのコンポーネント class AddTaskForm extends React.Component<IAddTaskFormProps, any> { private handleAdd = (e) => { e.preventDefault(); var newTask = ReactDOM.findDOMNode<HTMLInputElement>(this.refs["name"]).value.trim(); if (!newTask) { return; } this.props.onTaskAdd({ name: newTask }); ReactDOM.findDOMNode<HTMLInputElement>(this.refs["name"]).value = ""; } render() { return ( <div> <input type="text" placeholder="newTask" ref="name"/> <button onClick={this.handleAdd}>add</button> </div> ); } } // ルートコンポーネントの state のインタフェース interface ITaskAppState { tasks: ITask[]; } // ルートコンポーネント // 状態をまとめて管理する class TaskApp extends React.Component<any, ITaskAppState> { constructor(props: any) { super(props); this.state = { tasks: [ { name: "foo", completed: false, editing: false }, { name: "bar", completed: false, editing: false }, { name: "hoge", completed: false, editing: false }, { name: "fuga", completed: false, editing: false } ] }; } private handleTaskAdd = (newTask: ITask): void => { var newTasks = this.state.tasks.concat(newTask); this.setState({ tasks: newTasks }); } private handleTaskDelete = (task: ITask): void => { var newTasks = this.state.tasks.filter(t => { return t !== task; }); this.setState({ tasks: newTasks }); } private handleTaskSave = (task: ITask, newValues: ITask): void => { var newTasks = this.state.tasks.map(t => { if (t === task) { return _.extend(task, newValues); } else { return t; } }); this.setState({ tasks: newTasks }); } render(): JSX.Element { return ( <div> <AddTaskForm onTaskAdd={this.handleTaskAdd} /> <TaskList tasks={this.state.tasks} onTaskSave={this.handleTaskSave} onTaskDelete={this.handleTaskDelete} /> </div> ); } } ReactDOM.render( <TaskApp />, document.getElementById("content") );
イベントハンドラにメソッドを渡すと、 this が window になってしまっていてハマった。 this を束縛するためにアローファンクション使って定義して回避。
refs は型定義ファイルのせいで、 refs.name みたいにアクセスできない。 refs["name"] じゃないといけなかった。 こればかりは仕方ない。 props や state みたいにインタフェースを指定できれば良いんだけど。
それ以外はすんなり。
コンパイルも npm run build
で一発。
実行してエラー踏んで気付いてたようなミス(主に typo だけど)が、
コンパイルの時点で気付けて精神衛生上良かった。
ちなみに Visual Studio 2015 で書いたんだけど、間違ったコードは即時にエラーが表示されたり、 インテリセンスが効いたりして、 すこぶる良かった。