不慣れによる失敗、配列の宣言

まだTypeScriptが手に馴染むには書いた量が少なく、日々失敗が多い。

type Book = { title: string, vols: [string] };
type BooksPayload = { books: [Book] }; // 正解は、= { books: Book[ ] };

こんなコードを書いていた。意図としてはBookの配列をActionのpayloadに搭載したいために書いてるのだけど、その型宣言を[Book]と書いていました。また、Book.volsの[string]も間違いです。ここの正解はbooks: Book[ ](およびvols: string[ ])と書くこと。間違ったままだとこれは「一つだけBook型の要素を持つTuple型」の表記になっています。恐ろしいことに大概のケースでトランスパイルもできてしまうのですが、引っかかるのは以下の場合。

const books: Books = []; // エラー。never[]と[Book]は型が違うと怒られる

空の配列が代入できない。また、複数の要素を持っても当然NG。Flowの時にTupleがあるんだなーと流し見してたけどTypeScriptにもTupleがあるとは。Tupleは(もう何年も書いてないけど)Swiftを書くとAPIの戻値で頻出だった記憶がある。

再発防止策

配列の型宣言を、string[ ]とかとせずに、Arrayと徹底したほうが良いのかな?しかしtslint-config-airbnbが配列リテラルで書けって怒る。

Reducerを書くのにhandleAction"s"をやめた

redux-actionsを用いてActionCreatorとReducerを書いていますが、TypeScriptに移行して書き方がちょっと変わりました。

これまではActionCreatorをまとめて作るcreateActionsと、Reducerをまとめて作るhandleReducersを便利に用いてました。しかしTypeScriptに移行してみるとこのまとめた書き方は型情報をうまく活かせない。

// @types/redux-actionsのindex.d.tsより
export function handleActions<State, Payload>( // <- 型引数PayloadがreducerMap内で共通化
    reducerMap: ReducerMap<State, Payload>,
    initialState: State
): Reducer<State, Payload>;

ActionCreatorもReducerも、まとめるとどうしてもPayloadの型をアプリケーションワイドで大きなものにするか、anyで受けるかになってしまいます。アプリケーションワイドにすれば注目すべき情報の粒度が大きくなるし、anyで受けたら型の恩恵をうけられない。それならhandleAction(handleAction"s"ではなく)で個別にReducerを定義したほうがよさそう。

type State = Map<string, any>;
type MoveToPayload = { compiler: string, framework: string }; // 粒度が小さくany無しで書ける

export const moveToAction = createAction<MoveToPayload, string, string>(
    'MOVE_TO',
    (compiler, framework) => ({ compiler, framework }),
);

export const moveToReducer = handleAction<State, MoveToPayload>(
    moveToAction,
    (state, action) => action.payload ? state.merge(action.payload) : state,
    Map({}),
);

そうなるとActionCreatorとReducerはセットで書いたほうが、型も共通するしテストも一体だし、見通しが良いので格段に書きやすくなった気がします。

とはいえ、一つのReducerが一つのActionしか処理しないとなるとStoreを作るときにさすがに粒度があわない。小さなアプリであれば一つ、大きなアプリでも数個の名前空間にまとめたい。どうしたものか手段を探したけどしっくり来るものが見つからなかったので、最終的にはcreateStore(combineReducer())で複数名前空間を一つにまとめる前段階として、手書きで複数のアトミックなReducerを連結することにしました。

function reduceReducers<S>(reducers: Reducer<S, any>[]) {
    // Array#reduce()で連結して一個のReducerにまとめる
    return reducers.reduce<Reducer<S, any>>(
            (previousValue, currentValue) =>
                (state, action) => currentValue(previousValue(state, action), action),
            (state, action) => state, // <- reduceに初期値を省略するTypeScript型定義がなかった
        );
}

const app = reduceReducers<State>([
    // handleActionで作ったアトミックなReducerたちを以下に並べる。
    togglePageLayout,
    toggleAdjustPage,
    requestBooks,
    receiveBooks,
    moveToReducer,
    locationChangeReducer,
]);

// そしてStoreを作る。
const store = createStore(
    combineReducers({ router, form, app }), // react-router-redux + redux-form + アプリ
    composeWithDevTools(applyMiddleware(
        routerMiddleware(history),
        moveToMiddleware(),
    )),
);

// Containerではこんな感じ。connect()はreact-reduxのもの
export default connect(state => state.app.toJS(), () => ({}))(NavigationComponent);

結果として上記のように、アトミックなReducerの配列を数珠繋ぎすることになりました。結構、正解な気がする。

tsconfig.jsonでstrict: trueとするのは時期尚早だった

できるだけ厳しめ環境で書いた方がよろしかろうと、TypeScriptのトランスパイラオプションを最もstrictにしましたが、私の現時点でtsconfig.jsonで、strict: trueとするのは時期尚早でした。

  • tsconfig.jsonで、strict: trueとする
  • webpack.config.*.tsで、'path'と'webpack'をimportしているところで型定義が無いとエラーがでる
  • yarn add @types/webpack -Dとして型定義を追加する。dependenciesで@types/nodeが入る
  • @types/webpack-envと@types/nodeで、Moduleの定義がぶつかり、死ぬ。
  • @types/webpack-envは、HMRでmodule.hotを呼ぶために必要。
  • @types/webpackを抜く。yarn remove @types/webpack
  • ここでワークアラウンド二択
    • tsconfig.jsonから、strict: trueを抜く、もしくは
    • webpack.config.*.tsを、TypeScriptやめて普通のJavaScript(.js)にする。

当面の結論

@types/nodeと@types/webpack-envの激突の根本はグローバルスコープの競合のため。この解決は当事者間の折り合いを待つか、より根本解決として将来のTypeScriptでグローバルスコープの素敵ソリューションが提供されるのを期待するかというとこかなと。ワークアラウンドとして、webpack.configをJavaScriptにします。美学だけで、効能だけ考えれば無理にTypeScriptで書く必要性なかった。そして、yarn remove ts-node。tsconfig.jsonのstrict: trueはがんばって残す。

よってこの記事のタイトルは、正しくは「webpackの設定をTypeScriptにするのは時期尚早だった」。

Redux DevToolsの問題

ChromeのRedux DevToolsも、ワークアラウンド。yarn add redux-devtools-extension。

import { createStore, combineReducers, applyMiddleware } from 'redux';
import { routerReducer as router, routerMiddleware } from 'react-router-redux';
import { History } from 'history';
import { composeWithDevTools } from 'redux-devtools-extension';

import { app } from './AppReducer';
import { pushMiddleware } from './pushMiddleware';

export default function (history: History) {
    const store = createStore(
        combineReducers({ router, app }),
        composeWithDevTools(
            applyMiddleware(
                routerMiddleware(history),
                pushMiddleware(),
            ),
        ),
    );
    if (module.hot) {
        module.hot.accept('./AppReducer.ts', () => {
            store.replaceReducer(combineReducers({
                router,
                app: require<{app: typeof app}>('./AppReducer').app },
            ));
        });
    }
    return store;
}

型情報がしっかり提供されていて驚いた。

Reduxでの有用なTypeScriptサンプル

https://github.com/reactjs/redux/tree/master/test/typescript

Reduxのリポジトリの中、test/typescriptにReduxでの有用なTypeScriptサンプルがあります。強い型付けでのMiddlewareの書き方とか参考になるもの多い。また、これらのテストは動かさずにコードをコンパイラやLintにかけることでindex.d.tsをテストするという。こういうテストもあるのだなあ。

Deep Dive

Introduction · TypeScript Deep Dive

一冊まるごとオンラインで。著者の人は何かに困ってググると、各掲示板でたくさんの人に答えてるすごい人。PDF版もあった。

型強化の呪文。noImplicitAny

www.infoq.com

上記記事をたまたま読んで目にした呪文。noImplicitAny。

Over time, they reached the point of enabling advanced compiler options, such as –noImplicitAny to prevent the compiler from inferring an any type.

advancedってすごいな。なるほど。やってみましょう。また、ドキュメントを見ると、strictNullChecksなるものも。

// tsconfig.json
{
    "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react",
        "lib": ["es6", "dom"],
        "types": ["webpack-env", "jest"]
    }
}

ちょいちょい「暗黙のany」を怒るエラーがでてきました。たとえばこんなの。

export class Counter extends React.Component<{}, { counter: number }> {
    constructor(props) {
        super(props);
        this.state = { counter: 0 };
    }
// 省略
}

このconstructorの引数に型が無い。直すと以下。

export class Counter extends React.Component<{}, { counter: number }> {
    constructor(props: {}) {
        super(props);
        this.state = { counter: 0 };
    }
// 省略
}

結構な箇所を直したら、noImplicitAny=trueでもOKになった。明示的なanyはOK。

import { handleActions } from 'redux-actions';
import { LOCATION_CHANGE } from 'react-router-redux';
import { matchPath } from 'react-router';
import { Map } from 'immutable';

const decodeName = (pathname: string) => {
    const m = matchPath<{ name?: string; }>(pathname, { path: '/player/:name' });
    if (m) {
        return { ...m.params, name: m.params.name };
    }
    return {};
};

type AppState = Map<string, any>;

export const app = handleActions<AppState, any>(
    {
// 省略
        MOVE_TO: (state: AppState, { payload: { name } }) => {
            return state.merge({ name });
        },
        [LOCATION_CHANGE]: (state: AppState, { payload: { pathname } }) =>
            state.merge(decodeName(pathname)),
    },
    Map({}),
);

Redux ActionsのhandleActionsの型引数の2番目には、Payloadがきます。でもこれはMOVE_TOのときには、{ name } だし、LOCATION_CHANGEのときは{ pathname }だから、anyで受けたかった。で、明示しているからOKだった。

React RouterのmatchPathは面白くて、型引数にExpressスタイルのURL分解パラメータをあらかじめ型として持つ。'/player/:name'の結果を{ name?: string; }で受けたために、m.params.nameという取り方ができるようになる。

strictNullChecksは結構直すの簡単ながら、なるほどそこでnullの可能性あるかと気がつかしてくれる。とても有用。

結論

noImplicitAnyはtrueにするのが正義。strictNullChecksはさらにもっと実利がある。ほか、noImplicitThisやalwaysStrictも含めて、単にstrict=trueにすれば一番厳しい。

TypeScript 2.3 · TypeScript

TypeScriptでもリリースビルドする

TypeScriptの8つめ。

Productionをやります。

// webpack.config.production.ts
import * as path from 'path';
import * as webpack from 'webpack';

export default {
    entry: './ts/index.tsx',
    output: {
        path: path.resolve('static/js'),
        filename: 'bundle.js',
    },
    module: {
        rules: [{
            test: /\.(ts|tsx)$/,
            use: [
                'awesome-typescript-loader',
                'tslint-loader',
            ],
        }],
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js'],
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin({ sourceMap: false, comments: false }),
    ],
};

特に論点ないですね。

$ yarn add rimraf -D

rimrafをいつも使ってます。

// package.jsonの一部
    "scripts": {
        "build": "rimraf ./static/js/*; webpack -p --config webpack.config.production.ts"
    },

yarn run buildもショートハンドがあります。これはnpmと同じ。

$ yarn build

Atomを入れてTypeScriptを書く

TypeScriptの7つめ。

愛用のWebStormでTypeScriptを研究しつつ、おまけでAtomインスコしてサンプルを作るときの清書プラットフォームとして使って見ました。

Homebrew-Cask

私はHomebrewでNodeもYarnも入れてますので、Atomも。MacOS向けGUIインストーラーアプリをHomebrewでいれる拡張として、このHomebrew Caskを使います。

$ brew tap caskroom/cask
$ brew cask install atom

プラグインをガシガシいれます。入れたのは以下のものと、それらが依存するもの。

  • atom-typescript
  • linter-tslint
  • platformio-ide-terminal
  • file-icons

他にはちょこちょこ入れ出ししてみたけど、良いものが見つからず、上のでほぼいい感じ。

課題

「Go to Declaration」で型定義に飛んだりすることができなかった。調べるとバグなんだか、そうじゃないんだか。結構長い間苦労されているのかな?Atomでまだ解決できてない。

しかし、WebStormはザクザク型定義で飛んだり跳ねたりできる!すげーや商用(なのかな?)。

$ brew cask uninstall atom
$ rm -rf ~/.atom

一旦、Atomはアンインストール。また時機をみて試してみます。おそらくWebStormのサブスクリプションが切れるころ。