Help us understand the problem. What is going on with this article?

React JSX with TypeScript(1.6)

More than 3 years have passed since last update.

はじめに

つい先日, 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にも記載できるということなので, 以下のようにしてもよい.

tsconfig.json
{
  "compilerOptions": {
    "module": "umd",
    "jsx": "react"
  }
}

gulp-typescriptを使う場合

gulp-typescriptを使うと, ローカルインストールしたTypeScriptとtsconfig.jsonを渡すことができるので, 僕は手元で下記のgulpfile.jsを作り, gulp bundleを実行していた.

gulpfile.js
'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に追記しておく必要がある.

~/.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すげー.

tsx-react-example-capt01.png

なお, syntaxプラギンはJSX対応されているわけではないが、特にハイライトの結果に違和感は感じない.

実際に作成したブツ

これでJSXを作成する準備が整った. 今回はサンプルということでtodo的なやつを作ってみた.

Babel + React等で0.13系のJSXを書いたことがあれば, 殆ど違和感なくComponentを作ることができる.

Babelとの差異で言うと, ReactのComponentをextendsする際に, ジェネリクスで2つのtypeが要求される、という点だろう.
1つめがprops用, 2つ目がstate用のtypeである.

  • 本体
src/main.tsx
/// <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>
        );
    }
}
  • リストの要素
src/todoItem.tsx
/// <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の起点)
src/bootstrap.ts
/// <reference path="../typings/bundle.d.ts" />

import * as React from 'react';
import {Main} from './main';
React.render(React.createElement(Main), document.getElementById('main'));
  • HTML
dist/index.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使いの人は今のうちからチェックしておいたらどうだろう。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away