JSX と TypeScript の混合 Flux または悪魔合体

JSX + TypeScript の悪魔合体

ギョーム的に気持ちになったので JSX + TypeScript をはじめました。

導入にあたってチーム内への説明を兼ねたブログ。AltJSに対して ES でいいじゃん派ですが、自分の型需要に対して 現状の Flowtype が辛みしかないのでやむをえず。

動機

  1. 紆余曲折あって結局 React を使うことにした
  2. React Component には JSX with Babel を使いたい(手書きは無理だ)
  3. UI 以外のロジックを持ったモジュールは型の恩恵に預かりたい
  4. Flowtype つらい
  5. TypeScript かー
  6. UI 周りは JSX で、その他の堅いロジックは TypeScript で書けばいいのでは?
  7. 共存だ!!

メリットがあるのかも不明瞭ですが、分からないからこそ試してみようという感じです。JSX と TypeScript の境界 ( EventEmitter や Observable ) で型は溶けますが、UI 層でチマチマしたこと言ってると苦しいので any 上等。

TypeScript も JSX もコンパイルが必要ですが、2つを同時に行うソリューションはありません。正確には TypeScript の JSX 対応 fork があり、本家に Pull Request を投げているようですが先行き不明。

構成

ahomu/demo-ts-jsx-flux です。幸い、トリッキーな構成にはなりませんでした。


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 の延長線上にある感覚で書ける
  • interfaceenum のようなパーツはあるけど単に便利
  • オブジェクトの型付けは JSDoc で @typedef していたので嬉しい
  • module とかは ES6 準拠で1ファイル1モジュールの export を書いてると使わない
  • Rx.Observable<T> のようなジェネリクスがあると心が落ち着く
  • 既存の JavaScript を TypeScript に置き換えて型を書き足していくの苦行っぽい
  • 動かすだけなら拡張子変えて、最低限の any を振りまけば動く

型の宣言に苦労した

// 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 使う限り逃れられる気がしない。

次回は「俺のリアクティブフラックス」です。