LoginSignup
20
7

More than 5 years have passed since last update.

flowtypeとOpaque Type Aliases

Last updated at Posted at 2017-11-19

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として扱える機能です。
これを実際にコードに落とすと下記のようになります。

opaque.js
// @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

Anumber型の別名で、BAの別のため、B=A=numberな形になります。
Opaque Type Aliaseは同一ファイルではA=Bと通常のType Aliasesと同じ挙動になるため、let b: B = 1Bnumber型の値を代入することができます。

しかし、別ファイルではA≠Bという扱いになるため、let b: B = 1number型の値を代入するとエラーになってしまいます。

これにより例えば同一ファイルで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として振舞うことができます。

opaque.js
// @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を指定していなためAnumber型)として振舞うことはできませんが、CSuperTypeAを指定しているためAnumber型)として振舞うことができています。
もちろん、Opaque Type AliaseとしてA≠Bは維持されているため

let c: C = 1;

とした場合はエラーになります。

このようにSuperTypeをOpaque Type Aliasesで指定することによって、Aとして振る舞いたいが、Aとは違う型として扱いたい型を作ることができるようになります。

それではどこで使うのか実例を何点か挙げてみます。

実例1: 文字列型のIDを他の文字列型と区別する

一つ目の実例はIDの特殊な型を作る例です。
例えば、文字列型のIDの型と名前や住所など他の文字列型と区別がつかないため誤代入が発生します。

そこでOpaque Type Aliasesを使い、文字列型として扱いつつ、他の文字列型と区別できるように別名を付けてあげます。

id.js
// @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を区別をする例です。
例えばUseridProductidは別物になります。
しかし、これをどちらも文字列型として扱った場合に、本来は違うIDが混在できてしまう問題が発生します。

user.js
// @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
  };
}
product.js
// @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

そこで。上記のようにUserIdProductIdと別々のOpaque Type Aliasesで別名を付与することで、同じ文字列型のIDでも別の型として取り扱うことができます。
もちろん、どちらのIDも文字列型として振舞うことができるため、例えばロギングのときなどに特別な変換処理を入れなくてもそのままIDを出力することができます。

実例3: 述語を付与する

三つ目は既存の型に述語を付与する例です。
flowtypeで既存の型に述語を付与してassertを削除しよう で型に述語を付与する話を書きましたが、Opaque Type Aliaseを使用することでより簡易に書くことができます。

nes.js
// @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は素敵な機能なので、ぜひぜひ皆さんお使いください。

20
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
7