TypeScript
React
material-ui
jss

Material-UI v1.0をTypeScriptで使う

少し前にMaterial-UIのv1.0を使ってみる記事を書いたのですが、その後GitHub issuesで意外とTypeScript対応についてきちんとしているのを見つけたので、ひとまずの雛形コードを紹介します。

コード全体はこちら https://github.com/gcoka/mui-ts-sample

前の記事
Material-UI v1.x (beta) を導入する

下準備

気軽に試してもらえるように動かすための手順です。

  • create-react-appで環境作り
  • Material-UIのインストール

create-react-appで環境作り

create-react-appのTypeScript用スクリプトを使います

バージョン確認
$ create-react-app --version
1.4.1
# create-react-appコマンドをインストール
yarn global add create-react-app

# create-react-app-typescriptを使ってreactテンプレートを作成
create-react-app mui-ts-sample --scripts-version=react-scripts-ts

cd mui-ts-sample

# 動作確認
yarn start

Material-UIのインストール

動作確認した時のMaterial-UIのバージョンは1.0.0-beta.16でした。

yarn add material-ui@next
yarn add material-ui-icons

サンプルコード

とりあえず完成形のコードを見てください。
App.tsxを次の通り書き換えたら、yarn startで起動します。

内容は前回のサンプルとほぼ同じですが、冗長なものは取り除いています。
また、withStylesが生成するHOC(High-Order Component)に対する型付けは、知らないとうまく行かないポイントなので、コンポーネントの切り出しもしています。

src/App.tsx
import * as React from 'react';
import Button from 'material-ui/Button';
import DeleteIcon from 'material-ui-icons/Delete';

import { withStyles, WithStyles } from 'material-ui/styles';

const styles = {
  box: {
    margin: 10,
    padding: 10,
    border: 'solid 1px gray',
  },
  button: {
    margin: 10,
  },
  buttonWithHover: {
    margin: 10,
    // hoverも記述できる
    '&:hover': {
      backgroundColor: '#ff0000',
    }
  },
};

type ClassNames = keyof typeof styles;

interface FlatButtonsProps {
  onClick: () => void;
}

// Stateless Function Componentsの場合
const FlatButtonsSample = withStyles(styles)<FlatButtonsProps>(
  (props: FlatButtonsProps & WithStyles<ClassNames>) => {
    const classes = props.classes;
    return (
      <div className={classes.box}>
        {/* クリックイベントの処理はこんな感じ */}
        <Button onClick={props.onClick} className={classes.button}>Default</Button>
        <Button color="primary" className={classes.buttonWithHover}>Primary</Button>
        <Button color="accent"><DeleteIcon />削除</Button>
      </div>
    );
  }
);

interface RaisedButtonsProps {
  onClick: () => void;
}

// Componentクラスの場合
class RaisedButtonsSample extends React.Component<RaisedButtonsProps & WithStyles<ClassNames>, {}> {
  render() {
    const classes = this.props.classes;
    return (
      <div className={classes.box}>
        <Button raised={true} onClick={this.props.onClick} className={classes.buttonWithHover}>Default</Button>
        <Button raised={true} color="primary" className={classes.button}>Primary</Button>
        <Button raised={true} color="accent" className={classes.button}><DeleteIcon />削除</Button>
      </div>
    );
  }
}

// 1ファイル内で書くためにHOCを変数に入れておく
const HocRaisedButtonsSample = withStyles(styles)<RaisedButtonsProps>(RaisedButtonsSample);

// 通常は別ファイルにするので、その場合は以下のようにexportする
// export default withStyles(styles)<RaisedButtonsProps>(RaisedButtonsSample);

// withStylesに型注釈が必要な場合は、以下のようにする
// export default withStyles<{} & ClassNames>(styles)<RaisedButtonsProps>(RaisedButtonsSample);

// そして別ファイルでimport
// import RaisedButtonsSample from './components/RaisedButtonsSample';

class App extends React.Component {
  handleClick = () => {
    alert('Clicked!');
  }

  render() {
    return (
      <div>

        <FlatButtonsSample onClick={this.handleClick} />
        <HocRaisedButtonsSample onClick={this.handleClick} />
      </div>
    );
  }
}

export default App;

サンプルコード説明

TypeScriptはとにかく型付けがめんどくさい&何をつけたら良いのかわからない。というのがあると思います。
Material-UI v1.0ではそれを補助するために、WithStylesという補助メソッドが用意されています。

コードのこの部分でインポートしています。

import部分
import { withStyles, WithStyles } from 'material-ui/styles';

小文字のwithStylesはprops.classesを注入するためのHOC生成メソッドですが、
大文字のWithStylesはprops.classesの型情報を付与するための補助メソッドです。
このあたりは、うまくやるためのトリックとして取り入れることになるかと思います。

また、stylesにJSS用のCSS定義をしているのですが、これの型定義をわざわざ書くと考えるとかなりげんなりしますね。
しかし、TypeScriptの記法でkeyofをうまく使えば、ここも非常に楽をすることができます。

stylesの型情報を抽出しているところ
type ClassNames = keyof typeof styles;

このClassNamesの型情報を覗いてみると、次のようになっていて、props.classesの利用時にこの型情報がきちんと反映されます。

ClassNamesの型情報
type ClassNames = "box" | "button" | "buttonWithHover"

この辺りは、TypeScriptの型推論機能強化の恩恵ですね。

あと、注意するべきなのは、withStylesによって生成されたHOCについても型注釈をつけてやらないと、利用側が型情報を参照できなくなることに注意しなければなりません。

例えば、一般形として、サンプルコード内のRaisedButtonsSampleを別ファイルに記述すると次のようになります。

./components/RaisedButtonsSample.tsx
import * as React from 'react';
import Button from 'material-ui/Button';
import DeleteIcon from 'material-ui-icons/Delete';

import { withStyles, WithStyles } from 'material-ui/styles';

const styles = { /* ...省略... */};

type ClassNames = keyof typeof styles;

interface RaisedButtonsProps {
  onClick: () => void;
}

class RaisedButtonsSample extends React.Component<RaisedButtonsProps & WithStyles<ClassNames>, {}> {
  render() {
    const classes = this.props.classes;
    return (
      <div className={classes.box}>
        { /* ...省略... */}
      </div>
    );
  }
}

export default withStyles<{} & ClassNames>(styles)<RaisedButtonsProps>(RaisedButtonsSample);
src/App.tsx
import RaisedButtonsSample from './components/RaisedButtonsSample';

withStyles<{} & ClassNames>の部分については、調査不足ですが、こういう形で書かないと型チェックが通らないケースがあります。

総括

TypeScriptで書いているにもかかわらず、Material-UIを使う分においては、型注釈を書く労力は非常に少ないのではないかと思います。

TypeScriptでReactしている人にとっては、v1.0のリリースが非常に楽しみですね!

追記 2017/10/17

公式にTypeScriptのサンプルが掲載されましたね!
https://material-ui-next.com/getting-started/examples/