型の恩恵をうける

2018年6月5日(火)
佐々木 俊介
【最新JavaScript開発~ES2017対応モダンプログラミング】 株式会社インプレスR&Dより発行された「最新JavaScript開発~ES2017対応モダンプログラミング」の立ち読みコーナー第4回です。

第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に型定義が導入される場合でもこの点は同じになることが期待できます。

リスト4.1: flowのサンプル

 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のプリミティブ型があります。

リスト4.2: プリミティブ型

 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

リスト4.3: キャプション

 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型は全部の型のスーパータイプです。

リスト4.4: キャプション

 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という型もありますが多用しない方がいいでしょう。

リスト4.5: オブジェクト

 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}と同じです。型を合成するものです。

リスト4.6: ユニオン・インターセクション

 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]のように定義をします。

リスト4.7: 配列・タプル

 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という文字列しか入れることができません。

リスト4.8: リテラル

 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>のような形で定義することができます。

リスト4.9: クラス

 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では歴史的な経緯もあって、プリミティブを組み合わせたオブジェクトを使うプロダクトが多いです。そういう本来は型情報をもっていなかった物に型を付与できるという強みがあります。

リスト4.10: type

 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キーワードを使ってインターフェースを宣言します。インターフェースは、こういう名前と入出力を持ったメソッドを持っていますよ、ということを示します。

リスト4.11: 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ではバブルソートを実装していますが、ジェネリクスが無ければ型に依存しないように宣言をすることができませんでした。

リスト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型を読み込んでいます。

リスト4.13: export/import

 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を叩くだけです。

(次回へ続く)


1. この宣言よりも前にECMAScriptのコードがあってはいけません。またこのコメントが無いファイルの型宣言は他のファイルから参照される場合でも処理されません。これらは割と陥りやすいのでご注意ください。

2. よく他の言語でいうところのNullableですね。

高校生のときにパソ通にハマリ、その後紆余曲折を経てテキストエディタやMSXエミュレータその他を開発。技術者として勤務した後、現在はフリーランスエンジニア。技術同人誌や技術ブログやマッハ新書などを書いている。新しい技術に目が無い。アルゴリズム大好き。著書に『最新JavaScript開発~ES2017対応モダンプログラミング』(インプレスR&D)など。

連載バックナンバー

開発言語
第5回

ユニットテストをしよう

2018/6/11
【最新JavaScript開発~ES2017対応モダンプログラミング】 株式会社インプレスR&Dより発行された「最新JavaScript開発~ES2017対応モダンプログラミング」の立ち読みコーナー第5回です。
開発言語書籍・書評
第4回

型の恩恵をうける

2018/6/5
【最新JavaScript開発~ES2017対応モダンプログラミング】 株式会社インプレスR&Dより発行された「最新JavaScript開発~ES2017対応モダンプログラミング」の立ち読みコーナー第4回です。
開発言語書籍・書評
第3回

ECMAScript

2018/5/29
【最新JavaScript開発~ES2017対応モダンプログラミング】 株式会社インプレスR&Dより発行された「最新JavaScript開発~ES2017対応モダンプログラミング」の立ち読みコーナー第3回です。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています