今まで書いたサンプル程度の規模でも、
実行時に踏むまでバグに気付かないことが結構あって、
実際踏むと萎える。
React で規模の大きい SPA 作っていたらと思うとゾッとする。
コンパイル時に分かれば良いのに。
そうだ。
TypeScript で書いてみよう。
JavaScript のスーパーセットというアプローチが、
AltJS の中でも筋が良いと思っていたんだよな。
github.com
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 で書き換えてみた。
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;
}
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>
);
}
}
interface IEditTaskFormProps {
task: ITask;
onTaskSave: ITaskSaveCallback;
}
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>
);
}
}
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;
}
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>
);
}
}
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 で書いたんだけど、間違ったコードは即時にエラーが表示されたり、
インテリセンスが効いたりして、
すこぶる良かった。