15K 行のアプリを TypeScript 1.8 から 2.0 に移行してみた
先日 TypeScript の新しいメジャーバージョン 2.0 のコンパイラの beta 版がリリースされました.
コンパイラのチェックの強化や非 null 型,tagged union など,いくつかの機能が追加・強化され,自分の趣味プロジェクトでも恩恵に与れそうだったので試しに移行してみました.
移行してみたのは下記の Electron でつくり中の Twitter Client アプリ(React+Redux)で,全体で大体 15000 行ぐらいです.
下記の手順で修正してみました.
- コンパイラをアップデートしてビルドしてみる
--noImplicitThis
,--noUnusedLocals
,--noUnusedParameters
を有効にしてみる--strictNullChecks
を有効にして nullability をチェックするようにするinclude
で glob を使う- Tagged union types で Redux の Action types をリファクタリングする
- 引数リストに trailling comma を入れていく
- インターフェースやクラスメンバを
readonly
指定する
各段階でビルド&単体テスト確認をするのが大事だと思うので,なるべく細かく段階を分けました.コミットも大体各作業ごとになっているので,随時 diff のリンクを貼ります.
「説明しなくて良いからコードを見せろ」という猛者向けに,下記が今回行った作業の全 diff です.+-1000行弱ぐらいですね.
コンパイラをアップデートしてビルドしてみる
TypeScript 2.0 beta は下記のコマンドで入るので,サッと入れてとりあえず npm run build
してみます.
$ npm install --save-dev typescript@beta
TypeScript 2.0 では処理フローを見て型を付けるようになったので,この時点でビルドが失敗することがあります. 例えば下記みたいな場合です.
type T = 'aaa' | 'bbb'; const v: T = 'aaa'; switch (v) { case 'aaa': /* ここでは v の型は 'aaa' */ console.log('OK'); break; case 'bbb': /* ここでは v の型は 'bbb' */ console.log('OK'); break; default: /* ここでは v の型は never */ console.log('NOT OK', v); break; }
このように,制御フローを見て適宜型を切り替えてくれるようになりました.
上記のように v
の型として取りうる可能性のある型がなくなると never
という型になり,v
を参照しようとするとエラーになります.
never type も 2.0 で導入された機能です.
基本的に煩雑さが減る方向のはずなので,修正は容易なはずです.
--noImplicitThis
,--noUnusedLocals
, --noUnusedParameters
を有効にしてみる
2.0 ではいくつかのチェック機能がコンパイラに追加されました.互換性が理由(?)でデフォルトはオフのようですが,特に理由が無ければ有効にしておいたほうが良さそうです.
tsconfig.json で "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true
のように指定できます.
機能名の通り各チェックを行って,検知した時はコンパイルエラーに落としてくれます.特に noUnusedLocals
は tslint で検知してくれなかった未使用の import
を検出してくれて助かりました.
ここまではたとえ 2.0 にすぐに上げられないプロジェクトでも一時的にコンパイラのバージョンを上げてチェックさせることで恩恵に与れそうです.
--strictNullChecks
を有効にして nullability をチェックするようにする
2.0 では null
の型がそのまま null
となり,T | null
のように書くことで nullable な型を表現できます.また,undefined
についても同様に T | undefined
のように書けます.
さらに tsconfig.json に "strictNullChecks": true
を追加することで,デフォルトで全ての型を non nullable type として扱うようにできます.
基本的には
null
を代入しているのに nullable でない変数の型に| null
を追加するnull
が来る可能性があるのにチェックしていない箇所は素直にチェックする処理を入れる- 型は nullable だけど実質
null
な値が来ないところには null assertion 演算子(後置!
)を使う
という処理を入れていけば修正は OK です.3. は例えば下記のような感じです.
// 対象の要素が無い時 getElementById は null を返すので戻り値型は本来 // Element | null だが,静的に HTML に id が振られているので実質失敗しない // expr! で expr が null にならないとコンパイラに伝える const elem = document.getElementById('root')!;
また,null assertion 演算子は undefined
にも使えます.
変更箇所もなかなか多かったのですが,他にも細々とうまくいかないところがあり,この変更は思ったより大変でした.
例えば,strictNullChecks
によってオプショナル引数の型が以前と変わります.
function foo(optional?: number) { // 1.8 まで -> number // 2.0 から -> number | undefined optional; }
これだけ見ると問題ないように見えますが,型定義ファイルには,実質オプショナルでないのにオプショナル引数になっている定義が割といっぱいあります. 特にコールバックの引数の指定がまずいものが多く,1.8 までは型的には変わらなかったので問題にならなかったのですが 2.0 では問題になります.
今回例えば困ったのは react.d.ts
の StatelessComponent
です.
interface StatelessComponent<Props> { (props?: Props, context?: any): ReactElement<any>; // ... その他雑多なプロパティ }
上記の props?
が問題です.
const MyComponent: React.StatelessComponent<Props> = props => ...;
今までは上記のようにコンポーネントの型を指定し,引数を推論させていてこれでうまくいっていました.ですが,引数は props?
となぜかオプショナルに定義されているため,
2.0 からは props
の型は Props | undefined
に推論され,props
が undefined
でないかどうかのチェックを入れる必要が出てきてしまいます.
(僕の知識が足りないだけで実は props?
や context?
が実際に undefined
になるようなケースがひょっとしたらあるのかもしれないですが…もしそうなら教えていただけるとうれしいです)
仕方がないので下記のようにしてワークアラウンドを入れています.
const MyComponent = (props: Props) => ...;
これだと引数の型はプログラマが指定したものになるので undefined
チェックを回避できます.現状ではこれで問題になっていませんが,上記型定義の「その他雑多なプロパティ」がほしくなった場合は困ることになります.
また,react-redux.d.ts
の connect()
も困りました.connect()
は mapDispatchToProps
(第2引数)だけを指定したいときは第1引数を null
にできるのですが,第1引数の型定義に | null
が入っていないため
null
を渡せません.仕方ないので手元で react-redux.d.ts
に手を入れ,リポジトリに含めて typeings でインストールしたものの代わりにそちらを見るようにして凌いでいます.
型定義ファイルの更新状況などを見ながら,strictNullChecks
を付けるのは少し待ったほうが良いかもしれません.DefinitelyTyped では今のところ TypeScript 2.0 向けの型定義ファイルの開発を types-2.0 ブランチで進めているようです.
include
で glob を使う
TypeScript のプロジェクトファイル tsconfig.json で指定するソースコード一覧には今まで glob が使えなかったためファイルを1つずつ追加していました.
2.0 からは晴れて *
や **
が使えるようになったのでそれを使うようにします.
**/*.ts
や **/*.tsx
,typings/*.d.ts
のように指定すれば良くなりました.
Tagged union types で Redux の Action types をリファクタリングする
2.0 では tagged union types という機能が入り,union types のどの型の値が入っているのかを,各型に共通のプロパティで判断してくれます. プロパティには string literal type を指定すれば良いようです.公式の What's New に載っている例が分かりやすいです.
flux や redux では type
プロパティを見てアクションの種類を判断するため,アクションの型を tagged union type にすることでうまく型がつくようになります.
/* * action_type.ts */ // 'type' プロパティがタグになる type Action = { type: 'AddSomething', item: Something, } | { type: 'Sort', kind: Kind, } | { type: 'DeleteLast', }; export default Action; /* * actions.ts */ import Action from './action_type'; export function addSomething(item: SomeThing): Action { return { type: 'AddSomething', item, }; } export function sort(kind: Kind): Action { return { type: 'Sort', kind, }; } export function deleteLast(): Action { return { type: 'DeleteLast', }; } /* * reducer.ts */ import Action from './action_type'; function reduce(state: State = DefaultState, action: Action) { switch (action.type) { case 'AddSomething': // ここでは action の型は {type: 'AddSomething', item: Something} になる const next = ...; return next; case 'Sort': // ここでは action の型は {type: 'Sort', kind: Kind} になる const next = ...; return next; case 'DeleteLast': // ここでは action の型は {type: 'DeleteLast'} になる const next = ...; return next; default: return state; } }
大体こんな感じです.これで各アクションの処理内で間違ったプロパティにアクセスしていた時などは全てコンパイルエラーになります. 文字列リテラルが複数箇所にあるのが気になるかもしれませんが,全て string literal 型としてコンパイラがチェックするのでタイポしていてもコンパイルエラーになり安心です. 良い感じです.
ちなみに以前は type
プロパティが取る値を全部 Kind
というオブジェクトに入れて Kind.AddSomething
のように参照していたので,:%s/Kind\.\(\w\+\)/'\1'/g
のように置換して少しマクロを使ってやるだけで
かなり簡単に置き換えられました.Vim 便利.
引数リストに trailling comma を入れていく
個人的にかなり嬉しい機能なのですが,2.0 からは関数の引数に trailing comma が許されるようになりました(公式の例). (配列やオブジェクトの要素は前から trailing comma を許してます).
tslint がチェックしてくれるようになるのを待っても良いのですが,せっかくなので手で置き換えました.\h\w*\s*\(\n
あたりで適当に grep で引っ掛けて修正します.
Vim なら一度行末にコンマを追加する修正を入れれば,他の箇所は .
で繰り返すだけで OK ですね.Vim 便利.
インターフェースやクラスメンバを readonly
指定する
変更するつもりのないインターフェースのプロパティやクラスメンバに 2.0 から導入された readonly
指定をつけてまわります.基本的に大抵のプロパティは変更されないと思うので,デフォルトで readonly
付けるぐらいの勢いで良さそうです.:%s/^\s*\zs\ze\w\+?\=:/readonly /g
とかやると雑にプロパティに readonly
を付けられるので,付けてみてコンパイラに怒られたらそのフィールドを外す感じで機械的に作業できます.Vim 便利.
まとめ
最近ブログ書いてなかった(ネタはあったのに)ので,久々に人柱になった内容をまとめてみました.まだ beta ではありますが,今のところ変なバグは踏んでません.--strictNullChecks
以外はとりあえずやっておいて損は無さそうです.せっかくなので,いずれ 2.0 が正式リリースされた時に役立つと良いなぁと思っています.