はじめに
つい先日, TypeScript 1.5系の安定版がリリースされたばかりだが, 1.6系の話をしようと思う.
6月頃に「TypeScriptにReactのJSXサポートが載るぞ!」とアナウンスがあり, 「へー」と思っていたのだが, 久々にmasterを確認してみたら, 既にmergeされてたので触ってみた.
(Roadmapへの記載とmergeのタイミングにあまり差がなかった模様1)
折角なのでTypeScriptのJSXサポートを試してみましたよ、というお話. なお、作成した内容はQuramy/tsc-react-exampleに上げてます.
追記
このエントリを書いた直後に気づきましたが, 既にTypeScript の JSX/React サポートを試すに同様の内容が投稿されてました... orz
環境の準備
コードの話をする前に, TypeScriptでJSXを記述するための環境準備について.
なにはともあれ, TypeScript本体をレポジトリから持ってこないと話になら無い.
グローバルインストールする場合, 下記でnightlyのTypeScriptを持ってくる
npm install -g typescript@1.6.0-beta
tsc --version
で Version 1.6.0-beta
として、1.6系の値が確認できればOK.
ローカルインストールであれば, ↓ですね.
npm install typescript@1.6.0-beta --save-dev
以下の説明ではローカルインストールした前提で進めることとする.
tsconfig.json or tscのオプション
コマンドラインからtscでコンパイルする場合は, 下記のように--jsx
オプションにreact
を付与して叩けばよい
./node_modules/TypeScript/bin/tsc --jsx react --module umd main.tsx
一点注意すると, tscに渡すファイルは.ts
ではなく, .tsx
である.
.ts
だと, --jsx
オプションを与えていてもJSXのパーサーは動作しない.
--jsx
がコマンドラインオプションになっているということは, 当然tsconfig.jsonにも記載できるということなので, 以下のようにしてもよい.
{
"compilerOptions": {
"module": "umd",
"jsx": "react"
}
}
gulp-typescriptを使う場合
gulp-typescriptを使うと, ローカルインストールしたTypeScriptとtsconfig.jsonを渡すことができるので, 僕は手元で下記のgulpfile.jsを作り, gulp bundle
を実行していた.
'use strict';
var gulp = require('gulp'),
typescript = require('typescript'),
ts = require('gulp-typescript'),
browserify = require('browserify'),
source = require('vinyl-source-stream'),
del = require('del')
;
var project = ts.createProject('src/tsconfig.json', {typescript: typescript});
gulp.task('compile', function () {
var result = gulp.src('src/**/*{ts,tsx}')
.pipe(ts(project));
return result.js.pipe(gulp.dest('.tmp'));
});
gulp.task('bundle', ['compile'], function () {
var b = browserify('.tmp/bootstrap.js');
return b.bundle()
.pipe(source('bundle.js'))
.pipe(gulp.dest('dist'))
;
});
gulp.task('clean', function (done) {
del(['.tmp'], done.bind(this));
});
browserify周りはもうちょっと綺麗に書けそうな気もするが, あまり詳しくないので, これで許してください.
react.d.ts
いつも通りdtsmでgetしておくだけ.
dtsm init
dtsm install react/react.d.ts --save
エディタ
僕はVim使いなので, Vim用設定の話だけ書きます.AtomとかSublimeは知らん.
JSXでも補完を有効にしたいので, Vimプラギンtsuquyomiを使うこととする.
このプラギンは, ローカルインストールされたTypeScriptを優先的に利用して補完情報を取得するようにできているので, グローバルな環境を汚さずにTypeScriptの新機能を利用したいときにうってつけだ. 作成者本人が言うんだから間違いない.
なお, 前述したようにJSXオプションを利用するためには, 対象コードの拡張子が.tsx
であるため, FileTypeの指定を.vimrcに追記しておく必要がある.
NeoBundle 'Shougo/vimproc'
NeoBundle 'Quramy/tsuquyomi'
NeoBundle 'leafgarland/typescript-vim'
autocmd BufNewFile,BufRead *.ts set filetype=typescript
autocmd BufNewFile,BufRead *.tsx set filetype=typescript
ご覧の通り, ちゃんとJSX中でも補完が効きました. TSServerすげー.
なお, syntaxプラギンはJSX対応されているわけではないが、特にハイライトの結果に違和感は感じない.
実際に作成したブツ
これでJSXを作成する準備が整った. 今回はサンプルということでtodo的なやつを作ってみた.
Babel + React等で0.13系のJSXを書いたことがあれば, 殆ど違和感なくComponentを作ることができる.
Babelとの差異で言うと, ReactのComponentをextendsする際に, ジェネリクスで2つのtypeが要求される、という点だろう.
1つめがprops用, 2つ目がstate用のtypeである.
- 本体
/// <reference path="../typings/bundle.d.ts" />
import * as React from 'react';
import {TodoItem} from './todoItem';
interface ITodo {
description: string;
key: number;
}
export interface IMainState {
newItem?: {
description: string;
};
todoList?: ITodo[];
}
export interface IMainProps {}
export class Main extends React.Component<IMainProps, IMainState> {
state: IMainState = {newItem: {description: ''}, todoList: []}
constructor () {
super();
this.changeName = this.changeName.bind(this);
this.addItem = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
}
changeName (e: any) {
this.setState({
newItem: {
description: e.target.value
}
});
}
addItem () {
var list = this.state.todoList;
list.push({
description: this.state.newItem.description,
key: new Date().getTime()
});
this.setState({
todoList: list,
newItem: {description: ''}
});
}
removeItem (item: ITodo) {
var list = this.state.todoList.filter(i => i.key !== item.key);
this.setState({todoList: list});
}
render () {
var todoItems = this.state.todoList.map(item => {
return <TodoItem key={item.key} item={item} onRemove={this.removeItem} ></TodoItem>;
});
return (
<div>
<div>
<input type="text" placeholder="input new item" value={this.state.newItem.description} onChange={this.changeName} />
<button onClick={this.addItem} >add</button>
</div>
<ul>{todoItems}</ul>
</div>
);
}
}
- リストの要素
/// <reference path="../typings/bundle.d.ts" />
import * as React from 'react';
interface ITodo {
description: string;
}
export interface ITodoItemState {}
export interface ITodoItemProps {
item: ITodo;
onRemove?: (todo: ITodo) => any;
key?: number; // I think this prop is unnecessary, but unless it an error occurs in tsc.
}
export class TodoItem extends React.Component<ITodoItemProps, ITodoItemState> {
constructor () {
super();
this.removeItem = this.removeItem.bind(this);
}
removeItem () {
this.props.onRemove(this.props.item);
}
render () {
return (
<li>
<span> {this.props.item.description} </span>
<button onClick={this.removeItem} >delete</button>
</li>
);
}
}
- 起動するやつ(bundle.jsの起点)
/// <reference path="../typings/bundle.d.ts" />
import * as React from 'react';
import {Main} from './main';
React.render(React.createElement(Main), document.getElementById('main'));
- HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>tsc-react-example</title>
</head>
<body>
<div id="main"></div>
<script src="bundle.js"></script>
</body>
</html>
気になったところ
仮想DOMエンジン側に要素とDOMのひもづけを教えてあげた方が描画が早くなる関係上, ReactでリストをRenderするときは以下のように key=...
を指定する.
(指定しないとReactがwarning吐いてくる)
var todoItems = this.state.todoList.map(item => {
return <TodoItem key={item.key} item={item} onRemove={this.removeItem} ></TodoItem>;
});
ただ, 上記のコードを書いた所, TodoItem
クラスのprops interfaceにもkeyという名前のプロパティが無いと怒られた.
class TodoItem extends React.Component<{key: number;}, any> {...}
key
ってComponentのpropsとして扱うものなのか?という違和感を少し感じたので書き留めておく.
終わりに
環境準備も含めて, かなりあっさりJSX + TypeScriptを用意することができた.
実際にガリガリと.tsxを書くのは1.6がリリースされてからになるだろうけど, React使いの人は今のうちからチェックしておいたらどうだろう。
-
Roadmapへの記載は6/19, mergeされたが6/30 っぽい ↩