前回
の続き。前回のリポジトリを元にTypeScript版を作ってみたい:
ishikuro/thinking-in-react-webpack-minimum at typescript · GitHub
最終的なファイルツリー
. ├── dist │ ├── bundle.js │ └── index.html ├── package.json ├── README.md ├── src │ ├── app.tsx │ ├── tsd.json │ └── typings │ ├── react │ │ └── react.d.ts │ └── tsd.d.ts ├── tsconfig.json └── webpack.config.js
今回もサーバーは立ち上げずにブラウザで直接ローカルのindex.htmlファイルを開いて動作を確認する。
前回と比較すると、型定義ファイル(typings/
)とコンパイルオプション(tsconfig.json
)が追加されている。
TypeScriptで記述
webpack中やvimプラグインで使われるので先にtypescriptのnpmを入れておく。
npm install -g typescript
.vimrc
" syntax highlightを有効にする NeoBundle 'leafgarland/typescript-vim' " typescriptのインデントを適切にする NeoBundle 'jason0x43/vim-js-indent' " コード補完や定義/参照へのジャンプをできるようにする NeoBundle 'Quramy/tsuquyomi' " .tsxもtypescriptとして扱う autocmd BufNewFile,BufRead *.{ts,tsx} set filetype=typescript
型定義ファイルを追加
npm install -g tsd cd src/ tsd query react --action install --resolve --save
コンパイル方法
TypeScript内でJSXやReactを扱うために、様々なハックが存在していたが、2015年7月ころにtypescriptがJSXをネイティブサポートしたために収束した。
- typescript 1.6以上でコンパイルすればok, webpackで使う場合は
npm install --save-dev typescript@next
で入れる - 拡張子は.tsxにしておく
- webpackでの利用方法や、Reactのコーディング方法はts-loaderのtestで提示されている。
babelの代わりにts-loaderを使う
npm install --save-dev typescript@next ts-loader
tsconfig.json
を作成。(コンパイラオプションやリンカ的なものを指示するファイル)
{ "compilerOptions": { "jsx": "react" }, "files": [ "src/typings/tsd.d.ts" ] }
webpack.config.js
を更新。拡張子をtsxにして、ts-loaderを使うようにするだけ。(Option: コンパイル速度を上げるためにはexternal指定でreactをビルドに含めないようにする。html側で読み込む。参考: ReactとStylusをwebpackで使うための開発環境構築)
module.exports = { entry: [ './src/app.tsx' ], output: { path: 'dist', filename: 'bundle.js' }, resolve: { extensions: ['', '.tsx', '.ts', '.js'] }, externals: { react: 'React' }, module: { loaders: [ { test: /\.ts(x?)$/, loader: 'ts-loader' } ] } };
(Option: dist/index.html
reactを別ファイルで読み込む。)
<!DOCTYPE html> <html> <head> <title>Thinking in React</title> </head> <body> <script src="../node_modules/react/dist/react-with-addons.min.js"></script> <script src="bundle.js"></script> </body> </html>
src/app.tsx
のサンプル。ポイントは、propsなどの受け渡しをinterfaceで行うこと。これだけでもだいぶ人に伝えやすくなると思った。
/// <reference path="typings/tsd.d.ts" /> import * as React from 'react'; interface Props { content: string; } class MyComponent extends React.Component<Props, {}> { render() { return <div>{this.props.content}</div> } } React.render(<MyComponent content="Hello World" />, document.body);
あとはコンパイルして確認
webpack open dist/index.html
追記: ソースコードの変更点
import
/// <reference path="typings/tsd.d.ts" /> import * as React from 'react';
- 型定義ファイルの参照
import * as React
とやらないと駄目( JSX | TypeScript Deep Dive )
基本
interface PCRProps { category: string; key: string; } class ProductCategoryRow extends React.Component<PCRProps, {}> { render() { return (<tr><th colSpan={2}>{this.props.category}</th></tr>); } }
- propsやstateはinterfaceを作って明示する。
extends React.Component<P, S>
の形式でclassを作る。 - JSXのプロパティも型があって、
<th colSpan="2">
もエラーになったので{2}
としている。
ハマったところ
class SearchBar extends React.Component<SBProps, {}> { handleChange() { this.props.onUserInput( React.findDOMNode(this.refs.filterTextInput).value, React.findDOMNode(this.refs.inStockOnlyInput).checked ); } render() { return ( <form> <input type="text" placeholder="Search..." value={this.props.filterText} ref={"filterTextInput"} onChange={this.handleChange.bind(this)} /> <p> <input type="checkbox" checked={this.props.inStockOnly} ref="inStockOnlyInput" onChange={this.handleChange.bind(this)} /> {' '} Only show products in stock </p> </form> ); } }
- React 0.13からDOMNodeの取得方法が変更になっている (React v0.13 | React)
extends React.Component
から作る場合は、onChange={this.handleChange.bind(this)}
のようにコールバックに.bind
を自分で付けないとだめ
this.refsはランタイムで設定されるんだけど、TypeScriptコンパイラは分かってくれてないのでエラーを出す。
ERROR in ./src/app.tsx (98,37): error TS2339: Property 'filterTextInput' does not exist on type '{ [key: strin g]: Component<any, any>; }'. ERROR in ./src/app.tsx (98,54): error TS2339: Property 'value' does not exist on type 'Element'. ERROR in ./src/app.tsx (99,37): error TS2339: Property 'inStockOnlyInput' does not exist on type '{ [key: stri ng]: Component<any, any>; }'. ERROR in ./src/app.tsx (99,55): error TS2339: Property 'checked' does not exist on type 'Element'.
解決方法は不明。一応、エラーを出しつつも.jsファイルは作ってくれて、意図した動作もするんだけど、とても嫌な感じ。
this.refsは使わないように、handleChange(e)から、e.target.valueみたいに取得する方針にして回避する。
Stateの初期化
interface FPTState { filterText: string; inStockOnly: boolean; } class FilterableProductTable extends React.Component<FPTProps, FPTState> { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; } ...
- getInitialStateはES6的な記述では使えない。代わりにconstructorを使う
JSONとかJSオブジェクトの読み込み
var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; interface Product { category: string; price: string; stocked: boolean; name: string; } interface FPTProps { products: Product[]; } React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
- そのままだとPropsに流し込めないので、一旦interfaceでラップする。