flowtypeのv0.51.0からOpaque Type Aliasesという機能が入りました。
Opaque Type Aliases - Enforcing abstraction through the type system
これはA
という型にB
という別名をつけ、同一ファイルではA=B
として扱い、別ファイルではA≠B
として扱える機能です。
とても素敵な機能なのですが、あまり話題にならなくて寂しかったので、どうやって使うのか、どこで使うのかなんかを書こうと思います。
Opaque Type Aliase
Opaque Type Aliaseは先ほども書いた通り、A
という型にB
という別名をつけ、同一ファイルではA=B
として扱い、別ファイルではA≠B
として扱える機能です。
これを実際にコードに落とすと下記のようになります。
// @flow
type A = number;
opaque type B = A;
let b: B = 1; // Not Error
export type {B};
// @flow
import type {B} from './opaque.js';
let b: B = 1; // Error
A
はnumber
型の別名で、B
はA
の別のため、B=A=number
な形になります。
Opaque Type Aliaseは同一ファイルではA=B
と通常のType Aliasesと同じ挙動になるため、let b: B = 1
とB
にnumber
型の値を代入することができます。
しかし、別ファイルではA≠B
という扱いになるため、let b: B = 1
とnumber
型の値を代入するとエラーになってしまいます。
これにより例えば同一ファイルでFactoryを提供すれば、Factoryを経由しないと生成できない型を提供することができるようになります。
(厳密に言えばany
に型変換すれば生成することはできます。が、そうするとflow coverage
でよろしくないコードとして検出されることになるので適切にCIを回していれば問題ないと思います)
Super Type
先ほどの例ではOpaque Type Aliaseの基本的な使い方はわかりますが、どこで使えばいいのか良さがあまりわからないと思います。
Opaque Type Aliaseの真価を発揮するのはSuper Typeを組み合わせたところからになります。
opaque type B: SuperType = A;
Opaque Type Aliaseでは上記のようにSuperType
を別名に付けることができます。
SuperType
を付けると同一ファイルでは同じ挙動になりますが、別ファイルではA≠B
のままA
として振舞うことができます。
// @flow
type A = number;
opaque type B = A;
opaque type C: A = A;
export type {B, C};
// @flow
import type {B, C} from './opaque.js';
declare var b: B;
b.toFixed(); // Error
declare var c: C;
c.toFixed(); // Not Error
B
ではSuperType
を指定していなためA
(number
型)として振舞うことはできませんが、C
はSuperType
にA
を指定しているためA
(number
型)として振舞うことができています。
もちろん、Opaque Type AliaseとしてA≠B
は維持されているため
let c: C = 1;
とした場合はエラーになります。
このようにSuperType
をOpaque Type Aliasesで指定することによって、A
として振る舞いたいが、A
とは違う型として扱いたい型を作ることができるようになります。
それではどこで使うのか実例を何点か挙げてみます。
実例1: 文字列型のIDを他の文字列型と区別する
一つ目の実例はIDの特殊な型を作る例です。
例えば、文字列型のIDの型と名前や住所など他の文字列型と区別がつかないため誤代入が発生します。
そこでOpaque Type Aliasesを使い、文字列型として扱いつつ、他の文字列型と区別できるように別名を付けてあげます。
// @flow
export opaque type ID: string = string;
export function id(s: string): ID {
return (s: ID);
}
// @flow
import type {ID} from './id.js';
import {id} from './id.js';
type Entity = {
id: ID;
name: string;
}
let entity: Entity = {
id: id('abc'),
name: 'foo'
};
entity.id = entity.name; // Error
従来であればクラスを作成して区別をする必要があったのが、別名をつけるだけでプリミティブな型を維持しつつ特別な型にできるというのはポイントが高いです。
特にJavaScriptではJSONのエンコード/デコードは頻発するので、クラスではなく、そのまま扱えるプリミティブな型のまま扱える恩恵ははかりしれません。
実例2: Entity毎のIDを区別する
二つ目の実例はEntity毎のIDを区別をする例です。
例えばUser
のid
とProduct
のid
は別物になります。
しかし、これをどちらも文字列型として扱った場合に、本来は違うIDが混在できてしまう問題が発生します。
// @flow
export opaque type UserId: string = string;
export type User = {
id: UserId;
name: string;
};
export function user(id: string, name: string): User {
return {
id: id,
name: name
};
}
// @flow
export opaque type ProductId: string = string;
export type Product = {
id: ProductId;
name: string;
};
export function product(id: string, name: string): Product {
return {
id: id,
name: name
};
}
// @flow
import {user} from './user.js';
import {product} from './product.js';
var u = user('abc', 'foo');
var p = product('abc', 'foo');
u.id = p.id; // Error
そこで。上記のようにUserId
とProductId
と別々のOpaque Type Aliasesで別名を付与することで、同じ文字列型のIDでも別の型として取り扱うことができます。
もちろん、どちらのIDも文字列型として振舞うことができるため、例えばロギングのときなどに特別な変換処理を入れなくてもそのままIDを出力することができます。
実例3: 述語を付与する
三つ目は既存の型に述語を付与する例です。
flowtypeで既存の型に述語を付与してassertを削除しよう で型に述語を付与する話を書きましたが、Opaque Type Aliaseを使用することでより簡易に書くことができます。
// @flow
export opaque type NonEmptyString: string = string;
export function nes(s: string): Promise<NonEmptyString> {
return (s.length > 0)
? Promise.resolve(s)
: Promise.reject(new Error('string is not empty.'))
}
// @flow
import type {NonEmptyString} from './nes.js';
import {nes} from './nes.js';
function foo(str: NonEmptyString) {
str.charAt(0);
}
async () => {
foo(await nes('abc'));
};
これは先の記事と同じNon Empty Stringを実現する例ですが、こちらの方がより短くなっていることがわかるでしょうか。
今までは文字列であり、Non Empty Stringであるということをany
を使って表現していたのが、Opaque Type Aliasesを使うことでより型安全に実現することができました。
おまけ
先日、Opaque Type Aliasesの機能は良いんだけど、型を変換するだけの関数を何度も呼び出すのは嫌だよねーという話をしていたら良いものを教えてもらいました。
これなら関数のインライン展開してくれるし、flow+prepackというのがそのうち鉄板構成になるかもしれませんね。
(Opaque Type Aliasesに限らずflowtypeでゴリ押すために型のための関数はよく作るので)
そのうちflowtype+prepackの記事を書こうと思うので、今回は紹介まで。
まとめ
最後に一点だけ注意です。
Opaque Type Aliaseを使えばどんな特殊な型を作ることもできますが、やはり工数的な意味でコスパの悪いところまで作りむのはオススメできません。
できるなら汎用的に扱えそうなところや、金額や単位系などセンシティブなところに適用していくと素敵なflowtypeのコードになると思います。
というわけで、Opaque Type Aliasesは素敵な機能なので、ぜひぜひ皆さんお使いください。