型の恩恵をうける
第4章 型の恩恵をうける
C/C++、C#、Java、Scalaのような静的型言語は、ある程度の大規模な開発に向いているとされています。型情報によってコンパイル前に安全性を担保できるからです。ECMAScriptは動的型言語ですが、TypeScriptもしくはFlowを導入することで、JavaScriptの世界でも型の恩恵を受けることが可能です。
4.1 Flow
Flow は ECMAScript に型定義を拡張して静的な型チェックを行うための仕組みです。プロジェクトの一部分にだけ導入することもできるためにTypeScriptよりはカジュアルに使うことができましたが、最近はTypeScriptも同じようにゆるく導入が出来るようになりました。
筆者個人の意見としては、どっちを使っても似たようなものかなと思います。FlowはFacebook社のプロダクトなのでReactとの相性が多少良かったり、AngularではTypeScriptが公式に使われていたりします。好みで選んでも大体問題ないとは思いますが、どちらを採用するにせよ、固有のネタはあまり深追いすべきではありません。FlowもTypeScriptもわりと似たことが出来ますので、そういうほぼ同じような部分だけ使っていれば、将来的につぶしが効きやすいです。FlowはFacebookが開発した、ECMAScriptに型定義を拡張して静的な型チェックを行うための仕組みです。プロジェクトの一部分にだけ導入することもできるためにTypeScriptよりはカジュアルに使うことができます。また、Flowと同じくFacebookの開発しているReactとの相性が良いことも利点です。
Flowではflowコマンドで静的解析をします。構文が拡張されているためそのままではECMAScriptとしては正しく走りませんが、Babelとbabel-plugin-transform-flow-strip-typesというプラグインで拡張構文を削除できます。
インストールは少し面倒ですが、第2章でwaterslideを導入してあればflowを簡単にセットアップできます。
Flowを使う場合、特に便利なのがVSCodeです。Flow Laungage Supportプラグインを導入していればIDE上で解析してくれます。
$ ws new --use flow node my-project
$ cd my-project
$ npm test
Flowで型チェックを行うためには、コードの先頭1で/* @flow */か// @flowのようにコメントを書いて、そのファイルが静的解析の対象であるということを明示します。
変数に型宣言を付与する場合は、変数名の後に :string のように型名を書きます。この形式はTypeScriptでも同じです。将来的にECMAScriptに型定義が導入される場合でもこの点は同じになることが期待できます。
1: // @flow
2:
3: const hoge: string = 'hoge' // OK
4: const fuga: string = 1 // NG
5:
6: const piyo = (s: string): string => `Hello, ${s} world.`
7: piyo('hoge') // OK
8: piyo(1) // NG
このコードを用意してflowコマンドかws testを実行するとエラーが出ます。fuga:string で文字列であると宣言している変数に数字である1を代入しようとした為です。次にpiyoは引数と返り値が文字列として定義されているのですが、関数呼び出し時に数字を指定してるためにエラーになります。このように定義と食い違うアクセスをしようとするとエラーが出るのです。
4.1.1 指定できる型
指定できる型についてひととおり見ていきましょう。
プリミティブ型
まずboolean, number, stringのプリミティブ型があります。
1: const hoge: string = 'hoge'
2: const fuga: number = 42
3: const piyo: boolean = false
nullもプリミティブ型です。void型はundefinedを指します。null, voidを直接書かず、多くはnullやundefinedを許容する型宣言をします。これにはふたつのケースがあります。ひとつめは省略可能な仮引数を作る場合に、hoge?:stringというような宣言をするケースです。これはstringかundefinedのどちらかを指定できます。もうひとつはhoge:?stringのような宣言です。これはstring, undefinedに加えてnullも許容します2。
1: const hoge = (arg?: string) => {}
2: hoge('hoge') // OK
3: hoge() // OK
4: hoge(undefined) // OK
5: hoge(null) // NG
6: hoge(1) // NG
7:
8: const fuga = (arg: ?string) => {}
9: fuga('fuga') // OK
10: fuga() // OK
11: fuga(undefined) // OK
12: fuga(null) // OK
13: fuga(1) // NG
any, mixed
任意の型を表す逃げ道もあります。anyとmixedです。違いはmixed型を他の型の変数に代入することができないというものです。詳しくいうとany型は全部の型のスーパータイプかつサブタイプですがmix型は全部の型のスーパータイプです。
1: let n:number = 0
2: let _any:any = 0
3: let _mixed:mixed = 0
4:
5: n = _any // OK
6: n = _mixed // NG
7:
8: _any = n // OK
9: _mixed = n // OK
オブジェクト
オブジェクトを宣言することもできます。たとえばarg:{s:string, n:number}はargはsというメンバーがstringでnというメンバーがnumberなオブジェクトであるという型です。また、JavaScriptでは連想配列・Map代わりにオブジェクトを使うことも多いのですが、これもarg:{[string]:number}のように宣言することができます。また任意のオブジェクトにマッチするObjectという型もありますが多用しない方がいいでしょう。
1: const hoge = (arg: {s: string, n: number}) => {}
2: hoge({s: 'string', n: 0}) // OK
3: hoge({s: 1, n: 2}) // NG
4: hoge('string') // NG
5:
6: const fuga:{[string]: number} = {}
7: fuga['fuga'] = 1 // OK
8: fuga[1] = 1 // NG
9:
10: const piyo = (arg: Object) => {}
11: piyo({}) // OK
12: piyo({s: 'string'}) // OK
13: piyo({s: 1}) // OK
14: piyo('string') // NG
合成型(ユニオン・インターセクション)
ユニオン型は複数の型のうちどれか?という型です。hoge:string | numberであればhogeはstringかnumberのどちらかの型ということになります。
ユニオンと逆の働きはインターセクションです。これは指定した型のうち全部を満たすものという型です。hoge:{fuga:string} & {piyo:number}であれば実質hoge:{fuga:string, piyo:number}と同じです。型を合成するものです。
1: const hoge = (arg: string | number) => {}
2: hoge('hoge') // OK
3: hoge(1) // OK
4: hoge(true) // NG
5:
6: const fuga = (arg: {a: string} & {b: number}) => {}
7: fuga({a: 'string', b: 1}) // OK
8: fuga({a: 'string'}) // NG
9: fuga({b: 1}) // NG
配列
配列には二種類の記述方法があります。Array<string>とstring[]です。同じ物でどちらにもこの宣言をした配列にはstring以外を入れることができなくなります。また特殊な配列の使い方としてタプルがあり、複数の型が混在した固定長の配列です。arg:[string, number, boolean]のように定義をします。
1: const hoge = (arg: Array<string>) => {}
2: hoge(['string']) // OK
3: hoge([1]) // NG
4: hoge('string') // NG
5:
6: const fuga = (arg: [string, number, boolean]) => {}
7: fuga(['hoge', 1, true]) // OK
8: fuga([0, 1, 2]) // NG
9:
10: const piyo = (arg: [string, number, boolean], n: number) => arg[n]
11: const a: string | number | boolean = piyo(['fuga', 1, true], 0) // OK
12: const b: number = piyo(['fuga', 1, true], 0) // NG
リテラル
リテラルを指定することもできます。たとえば、hoge:'string'であれば、hogeにはstringという文字列しか入れることができません。
1: const hoge = (arg: 'hoge') => {}
2: hoge('hoge') // OK
3: hoge('fuga') // NG
4: hoge(1) // NG
5:
6: const fuga = (arg: 1 | 'fuga') => {}
7: fuga(1) // OK
8: fuga('fuga') // OK
9: fuga(2) // NG
10: fuga('hoge') // NG
クラス
最後はクラスです。クラス名をそのまま型として指定すると、そのクラスから生成されたインスタンスを納めることができます。hoge:HogeにはHogeクラスかそのサブタイプのみを格納することができます。それではクラス自体を変数に入れたい場合はどうでしょうか?これはClass<Hoge>のような形で定義することができます。
1: class Hoge {
2: hoge(arg: string) {}
3: }
4:
5: class Bad {}
6:
7: const hoge: Hoge = new Hoge() // OK
8: const Fuga: Class<Hoge> = Hoge // OK
9: const fuga: Hoge = new Fuga() // OK
10: const piyo: Hoge = new Bad() // NG
4.1.2 型を定義する
型は変数に付与するだけではありません。独自の型を定義できます。まずはtypeキーワードを使った型の宣言です。JavaScriptでは歴史的な経緯もあって、プリミティブを組み合わせたオブジェクトを使うプロダクトが多いです。そういう本来は型情報をもっていなかった物に型を付与できるという強みがあります。
1: type num = number
2: const hoge: num = 1 // OK
3:
4: type fuga = {
5: s: string,
6: n: ?number
7: }
8:
9: const fuga1: fuga = {s: 'fuga'} // OK
10: const fuga2: fuga = {s: 'fuga', n: 1} // OK
11: const fuga3: fuga = {n: 1} // NG
次はinterfaceキーワードを使ってインターフェースを宣言します。インターフェースは、こういう名前と入出力を持ったメソッドを持っていますよ、ということを示します。
1: interface Hogable {
2: hoge(arg: string): number
3: }
4:
5: class Hoge {
6: hoge(arg: string) {
7: return Number.parseInt(arg)
8: }
9:
10: fuga(arg: number) {
11: return arg.toString(2)
12: }
13: }
14:
15: const hoge: Hogable = new Hoge() // OK
16: hoge.hoge('hoge') // OK
17: hoge.fuga(1) // NG
hogeはインターフェースであるHogableとして定義されています。HogeクラスはHogableの条件を満たすので、hogeにインスタンスを格納できます。
Hogableはhoge()メソッドを定義してるためhoge.hoge('hoge')は大丈夫なのですが、fuga()メソッドは定義されていないのでエラーになるのです。ただ、これはあくまでFlowでの判定なので、実際にはhogeはfuga()メソッドを持っているためにこのコードではエラーが出ずに正常に実行されます。
4.1.3 ジェネリクス
ジェネリクスはコンテナやアルゴリズムなどで使われるもので、特定の型に依存しない抽象的なものです。リスト4.12ではバブルソートを実装していますが、ジェネリクスが無ければ型に依存しないように宣言をすることができませんでした。
1: function sort<T>(data: Array<T>, compare: (a: T, b: T) => number) {
2: for (let x = 0; x < data.length; x++) {
3: for (let y = 0; y < data.length; y++) {
4: if (compare(data[x], data[y]) < 0) {
5: const c = data[y]
6: data[y] = data[x]
7: data[x] = c
8: }
9: }
10: }
11:
12: return data
13: }
14:
15: const comp = (a: number, b: number) => a - b
16: const result = sort([4, 2, 3, 1], comp)
17: console.log(result)
4.1.4 型をexport/importする
型情報を宣言してもそのままでは別のファイルからはアクセスができません。exportとimportが必要になります。リスト4.13ではstr型とnum型がexportされていて、同じディレクトリにあるbool.jsからbool型を読み込んでいます。
1: type str = string
2: export type str
3: export type num: number
4:
5: import type bool from './bool'
それでは、npmで読み込む外部のライブラリについてはどうすればいいのでしょうか。Flow-Typedというソフトを使うことでプロジェクトで依存しているパッケージについて、型情報があれば取得し、なければ作ってくれます。
$ npm install flow-typed -g
$ flow-typed install
グローバルにflow-typedをインストールして、プロジェクトディレクトリ上でflow-typed installを叩くだけです。
(次回へ続く)