JSX と TypeScript の混合 Flux または悪魔合体
Posted: Updated:
JSX + TypeScript の悪魔合体
ギョーム的に気持ちになったので JSX + TypeScript をはじめました。
導入にあたってチーム内への説明を兼ねたブログ。AltJSに対して ES でいいじゃん派ですが、自分の型需要に対して 現状の Flowtype が辛みしかないのでやむをえず。
動機
- 紆余曲折あって結局 React を使うことにした
- React Component には JSX with Babel を使いたい(手書きは無理だ)
- UI 以外のロジックを持ったモジュールは型の恩恵に預かりたい
- Flowtype つらい
- TypeScript かー
- UI 周りは JSX で、その他の堅いロジックは TypeScript で書けばいいのでは?
- 共存だ!!
メリットがあるのかも不明瞭ですが、分からないからこそ試してみようという感じです。JSX と TypeScript の境界 ( EventEmitter や Observable ) で型は溶けますが、UI 層でチマチマしたこと言ってると苦しいので any 上等。
TypeScript も JSX もコンパイルが必要ですが、2つを同時に行うソリューションはありません。正確には TypeScript の JSX 対応 fork があり、本家に Pull Request を投げているようですが先行き不明。
構成
ahomu/demo-ts-jsx-flux です。幸い、トリッキーな構成にはなりませんでした。
こんな感じ。あと特筆すべきは以下くらい。
- TypeScript の SourceMaps を browserify に引き継ぐためインライン化が必要
- テストは ES6 で書くが対象は
build
から直接 import する - 拡張子で TypeScript か Babel に振り分ける transform を用意するとなおよい
JSX ↔ TypeScript 間のモジュール共有
- 主な対象は Component ( JSX ) からの Store や Action ( TypeScript ) の呼び出し
- モジュールの import/export は、ES6 modules 準拠のみ使う
- TypeScript から JSX を呼ぶとコンパイル時の解析でエラーになる
- が、TypeScript で書かれるべきロジックは Component を呼ぶことはない
- 呼び出してしまったらコード設計的にそもそもおかしい
default.default error
で実際に書いてみるとエラーになる。
// awesome.ts export default AwesomeStore; // component.jsx import AwesomeStore from '../stores/awesome.ts'; // => undefined error!
ES6 の import Foo from 'foo';
の記法は、foo.js が {default: Foo}
を export することを期待する。
interopェ...
TypeScript の ES6 module export は変換時にしっかり {default: MessageComposer}
として export する。
// export default MessageComposer; は ↓ のようなイメージ export = { default: MessageComposer };
Babel の interop は __esModule
が生えていないオブジェクトは、{default: obj}
に変換して対処する。これは元々、npm モジュールなどで単に module.exports = AwesomeStore
としか書かない文化との互換性を狙っている。
var _interopRequireWildcard = function _interopRequireWildcard(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; };
TypeScript によって変換されたコードは __esModule
を持たない。よって、Babel によって default ラップされるので {default: {default: AwesomeStore}}
のような二重構造に変換されて死ぬ。
interop を無効にしよう
If you don't want this behaviour then you can use the commonStrict module formatter. Modules · Babel
だそうなので babel -m commonStrict
によって解決する。ただしこの場合、interop
によってチョロまかされていた npm モジュールが素直に import できなくなる。
// awesome.ts export default AwesomeStore; // component.jsx import AwesomeStore from '../stores/awesome.ts'; /* ok */ import * as React from 'react'; /* ng */ import React from 'react';
だがまぁ、TypeScript は default チョロまかし機能がなく import * as Rx from 'rx';
を元から強いられていたので、本来的な状態で足並みが揃ったといえる。
関連: ES6 Modules default exports interop with CommonJS · Issue #2719 · Microsoft/TypeScript とか。
TypeScript 所感
- 少なくとも v1.5 ならば ECMAScript 6 の延長線上にある感覚で書ける
interface
やenum
のようなパーツはあるけど単に便利- オブジェクトの型付けは JSDoc で
@typedef
していたので嬉しい module
とかは ES6 準拠で1ファイル1モジュールの export を書いてると使わないRx.Observable<T>
のようなジェネリクスがあると心が落ち着く- 既存の JavaScript を TypeScript に置き換えて型を書き足していくの苦行っぽい
- 動かすだけなら拡張子変えて、最低限の
any
を振りまけば動く
型の宣言に苦労した
- npm や
window
直下に生えた外部モジュールを扱うときに、型情報の提供が必要 - 型情報を提供する TypeScript ファイルは
.d.ts
という拡張子をもつ(慣習?) - メジャーなものは borisyankov/DefinitelyTyped に集約されている
- 型情報が必要なファイルで
/// <reference path="../typings/node/node.d.ts" />
のように書いて.d.ts
を呼び出す - DefinitelyTyped にないパーツに関する
.d.ts
の手書きがクセもの - [WIP] start support for tsc 1.5.0-beta by vvakame · Pull Request #4101 · borisyankov/DefinitelyTyped 大変そう...
// CommonJS で ↓ のように export されたものを想定 module.exports = IDBWrapper; // class 実装の宣言 declare class IDBWrapper { constructor(options: any); put(dataObj: any, success?: (id: string) => void, error?: Function): void; } // 謎宣言・もうちょっとマシな書き方できないの... declare module IDBWrapper { } // 公開 module の 宣言 declare module 'idb-wrapper' { export = IDBWrapper; }
一般的な npm モジュールを import * as IDBWrapper from 'idb-wrapper'
で呼ぶときが奇怪になった。だれか解説と正答を教えてけれ・・・。
vvakame/dtsm 便利
依存解決してくれるので DefinitelyTyped/tsd より優れています。使いましょう。
npm i -g dtsm dtsm init dtsm install react --save
カンタン。
Decorators の使い道
jayphelps/core-decorators.js とか見てると興味深いです。@abstract
とか @mixin
とかも作れそうです。前者はサブクラスが実装してなかったら警告を出す、みたいな感じに作れました。
@Component({
selector: 'tabs'
})
@View({
template: `
<ul>
<li>Tab 1</li>
<li>Tab 2</li>
</ul>
`
})
class Tabs { }
Angular JS 2.0 が必要としているコレですね。
登場人物の整理
登場する技術スタックが多いので、簡単な説明を付け加えます。
React
- みんな大好き View コンポーネントライブラリ
- View コンポーネントの単位で開発するための基本的な機能
- Virtual DOM という機構が有名
- 開発者が雑なロジックで DOM の更新を適用できるようにした
el.innerHTML = template(data)
の賢い版
今話題のReact.jsはどのようなWebアプリケーションに適しているか? Introduction To React─ Frontrend Conference | HTML5Experts.jp とか見たら良いと思います。
Flux
- React (または類似品) を使ったときの推奨アーキテクチャ
- View → Action → Dispatcher → Store → View .... という1方向のデータフロー基盤
- Viewの立場から見ると、リアクティブなデータフローに見えなくもない
- facebook/flux はあるけど、世間には大量の独自 Flux が跋扈する
Fluxとはなんだったのか + misc at 2014 - snyk_s log とか見たら良いと思います。
Browserify
- JavaScript の依存解決をして、ひとつのファイルにまとめてくれる
- CommonJS スタイル (
require()
とかmodule.exports
) と仲良し - npm と仲良しなので、近年の JavaScript 開発者には人気がある
流行り廃りガーと言われる JS 業界ですが、Browserify って実は古参ですよね。今の使い方で定着したのは比較的近年な気もするけど、もとは 2010〜2011 年くらい。
ES6 ( ECMAScript 6 )
- ECMAScript は言語仕様
- JavaScript は ECMAScript に準拠した言語実装
- 現在普及しているのは ECMAScript 5
- 近代的な言語機能を足した新バージョンが ECMAScript 6
基本的に ES6 を主軸においたコードを書くことを前提としたい。標準は正義。詳しくは WEB+DB PRESS Vol.85 の連載記事 をご覧ください。 :-)
原初の JavaScript からコア仕様を抜き出して、標準化による互換性を狙って作られたが ECMAScript であり、卵が先か鶏が先かといえば鶏が先っぽい。
JSX with Babel
- フロントエンド界に Facebook が提供した気持ち悪い技術要素の筆頭
- JSX 自体は Virtual DOM を生成するコードを記述するための DSL
- 見た目は禁断の HTML in the JavaScript そのもの
$el.append('<div>text</div>')
から脱却するためにかけた時間は何だったのか
"I can't decide if I don't like React or I don't like JSX. I'm leaning towards the latter." に見る、技術ではなく関心の分離だ派にとって、HTML は JavaScript でコントロールする対象でしかないのでしょう
とはいえ JSX は Virtual DOM の記述と、ひいては Virtual DOM が実現するおおざっぱな View 更新メカニズムのパフォーマンスを確保するための必要悪です。(個人の感想です)JSX はコンパイラによって次のように変換します。
// Before: JSX
return (
<div className="section">
<h3 className="thread-heading">{this.state.thread.name}</h3>
<ul className="list" ref="messageList">{messageListItems}</ul>
</div>
);
// After: JavaScript return React.createElement( 'div', { className: 'section' }, React.createElement('h3', { className: 'thread-heading' }, this.state.thread.name), React.createElement('ul', { className: 'list', ref: 'messageList' }, messageListItems) );
Babel · The compiler for writing next generation JavaScript は前述の ECMAScript 6 で書かれたコードを ECMAScript 5 相当にコンパイルするライブラリです。なぜか React の JSX コンパイルをサポートしています。
TypeScript
- みんな大好きな型検査がついた JavaScript
- 出始めこそ AltJS らしい風情だったが基本的には ECMAScript のスーパーセット
- 最新の v1.5 では ECMAScript 6 に追従した構文を多数サポート
- コンパイラが遅い問題はだいぶ前に劇的改善してた(今のところ気にならない)
interface Person { firstname: string; lastname: string; } function greeter(person : Person) { return `Hello ${person.firstname} ${person.lastname}`; } var user = {firstname: "Jane", lastname: "User"}; document.body.innerHTML = greeter(user);
型いです。じつに型い。調子にのって ES7 相当の Decorator をサポートしていますが、Babel も実験的にサポートを開始しているのでおあいこですね。
結論
ECMAScript 6 は、TypeScript と JSX with Babel の共通するお父さんです。けど、3人が一緒に住める家を作るのはめんどくさいです。
TypeScript が辛かったら、型を脱がせば普通の ES6 にできるはずなので、いつでも逃げる準備はできています。JSX は React 使う限り逃れられる気がしない。
次回は「俺のリアクティブフラックス」です。