flowtypeのUtility Typeについて その1

  • 40
    いいね
  • 0
    コメント

https://github.com/facebook/flow/releases/tag/v0.31.0

flowtype v0.31.0がでましたね!(既にv0.32.0もでてますけど)
CHANGELOG読んでみると

Added a new "magic" type called $PropertyType. This utility extracts the type of the property 'x' off of the type T.

magic typeなる気になる用語があるじゃありませんか!!
どうやら$から始まる定義済みの型のことを指してるようなのですが、公式のドキュメントや過去のCHANGELOGを見てもそんなものは一切出てきません。
仕方ないので実際にコードを見てみると

type_annotation.mlの中に$から始まる型があるので、名前とコメントから推測しつつ、flowtype.orgのtryで確認しながらどういった型なのか調べてみました。

ちなみにですが、flowtypeのドキュメント的にはUtility Typeというが正しいようです。(2017/01/07追記)

$Either<...T>

$Either|と等価で指定した型のいずれかを表す型(Union type)になります。

# Try Flow

type A = {a: number, b: string};
type B = {b: string, c: number};
var a: $Either<A, B> = {a: 1, b: 'hoge'};
var b: $Either<A, B> = {b: 'fuga', c: 2};
var c: $Either<A, B> = {a: 3, b: 'fuga', c: 4};
var d: $Either<A, B> = {a: 5, c: 6}; // Error !!

この例のように$Eitherを利用するとAまたはBと一致する構造のObjectを取ることができます。
flowtypeの仕様としてObjectは部分型になっており、同一のプロパティがあればその型として扱うことができます。
そのため、変数cではABに一致しているのためエラーにはなりません。

厳密に型チェックしたいときは後述のExact typeを使用してください。

# Try Flow

/* @flow */

type A = {type: 'hoge', num: number};
type B = {type: 'fuga', str: string};
var a: $Either<A, B> = {type: 'hoge', num: 1};

a.type;
a.num; // Error !!
a.str; // Error !!

if (a.type === 'hoge') {
  a.num;
  a.str; // Error !!
}
if (a.type === 'fuga') {
  a.num; // Error !!
  a.str;
}

また、実際に利用する場合は型を判別する必要があります。
判別前の場合では変数aに入っている値がAなのか、Bなのか判別できないため、共通しているキー以外はエラーが発生します。
そこで変数aABどちらかの型であることをifを使いDynamic Type Testで判別することで、typehogeAならプロパティnumが利用できるようになり、typefugaBならプロパティstrが利用できるようになります。

$All<...T>

$All&と等価で指定した型の全てと一致する型(Intersection Type)になります。

# Try Flow

type A = {a: number, b: string};
type B = {b: string, c: number};
var a: $All<A, B> = {a: 1, b: 'hoge'}; // Error !!
var b: $All<A, B> = {b: 'fuga', c: 2}; // Error !!
var c: $All<A, B> = {a: 3, b: 'piyo', c: 4};
var d: $All<A, B> = {a: 5, c: 6}; // Error !!

この例のように$Allを利用するとABの両方に一致する構造のObjectを取ることができます。
$Eitherとは違い型の判別は必要ないため、abcのキーはそのまま利用することができます。

# Try Flow

type A = {a: number, b: string};
type B = {b: boolean, c: number};
var a: $All<A, B> = {a: 1, b: 'hoge', c: 2};
var b: $All<A, B> = {a: 1, b: true, c: 2}; // Error !!
var c: $All<B, A> = {a: 1, b: true, c: 2};

また、同名のキーがある場合は先に指定したキーの型が優先されます。

$Tuple<...T>

$Tuple[...T]と等価で要素毎に違う型を指定できる型です。(Array<T>の場合は要素全てがTである必要がある)

# Try Flow

js
var a: [number, string, boolean] = [1, 'hoge', true];
var b: [number, string, boolean] = [1, 2, true]; // Error
var c: [number, string, boolean] = [1, 'hoge', 2]; // Error

基本的にTupleのような構造を利用するときは名前をつけてオブジェクトにするので使いどころがわかりません。

# Try Flow

~~```js
var a: [number, string, boolean] = [1, 'hoge', true];

a.map((x: number | string | boolean) => x);
a.map((x: number) => x); // Error !!
a.map((x: string) => x); // Error !!
a.map((x: boolean) => x); // Error !!
```~~

[number, string, boolean]Tuplemapで回すときも引数はnumber | string | booleanの型である必要があります。
やっぱり、いまいち利用所が思いつかないです。

こういう値を返すライブラリで使えるかもしれませんが、そんなライブラリは投げ捨てた方が幸せになれると思います。

$Tupleは削除されました。[A, B]を使いましょう。

$Supertype

https://flowtype.org/docs/utility-types.html#supertypet

$Supertypeclass Klass<-T> {}と同様に反変を表す型です。

# Try Flow

class Hoge {};
class Fuga extends Hoge {};

class Klass1<-T> {
  fn(x: T): void {}
}
var a1: Klass1<Hoge> = new Klass1();
var a2: Klass1<Fuga> = a1;
var a3: Klass1<Fuga> = new Klass1();
var a4: Klass1<Hoge> = a3; // Error !!

class Klass2<T> {
  fn(x: T): void {}
}
var b1: Klass2<Hoge> = new Klass2();
var b2: Klass2<$Supertype<Fuga>> = b1;
var b3: Klass2<Fuga> = new Klass2();
var b4: Klass2<$Supertype<Hoge>> = b3; // Error !!

-Tの場合には型パラメーターに指定できますが、例えば変数の型や、関数の戻り値には指定できません。
その一方で$Supertypeで型を指定した場合には変数の型などに指定できる代わりに、型パラメーターには指定ができなくなります。

$Subtype

https://flowtype.org/docs/utility-types.html#subtypet

$Subtypeclass Klass<+T> {}と同様に共変を表す型です。

# Try Flow

class Hoge {};
class Fuga extends Hoge {};

class Klass1<K, +V> {
  x: { [k: K]: V};

  fn(k: K): V {
    return this.x[k];
  }
}
var a1: Klass1<string, Hoge> = new Klass1();
var a2: Klass1<string, Fuga> = a1; // Error !!
var a3: Klass1<string, Fuga> = new Klass1();
var a4: Klass1<string, Hoge> = a3;

class Klass2<K, V> {
  x: { [k: K]: V};

  fn(k: K): V {
    return this.x[k];
  }
}
var b1: Klass2<string, Hoge> = new Klass2();
var b2: Klass2<string, $Subtype<Fuga>> = b1; // Error !!
var b3: Klass2<string, Fuga> = new Klass2();
var b4: Klass2<string, $Subtype<Hoge>> = b3;

$Subtype$Supertypeと同様に+T$Subtype<T>で指定できるところが違います。

$Type

$TypeTと一致した型を取る型です。

# Try FLow

class A { }
class B extends A { }
class C extends B { }

var a1: $Type<B> = A; // Error!
var a2: $Type<B> = B;
var a3: $Type<B> = C; // Error!
var b1: typeof B = A; // Error!
var b2: typeof B = B;
var b3: typeof B = C;
var c1: Class<B> = A; // Error!
var c2: Class<B> = B;
var c3: Class<B> = C;

似たようなものでtypeofClass<T>もありますが、それらと違い$Typeが厳密に一致する型をとります。
そのため、継承関係にある子のクラスをTに渡したさいに、typeofClass<T>では通りますが、$Typeではエラーとなります。

$PropertyType

https://flowtype.org/docs/utility-types.html#propertytypet-x

$PropertyTypeTに指定した型のプロパティxの型を取得する型です。

# Try Flow

type A = {x: number};

var a: $PropertyType<A, 'x'> = 1;
var a: $PropertyType<A, 'x'> = 'aaa'; // Error !!

ここでプロパティ名に指定できるのはString literalのみになるため、String literalのUnion typeなどは指定できません。
そのため、残念ながら後述の$Keysと組み合わせて、オブジェクトのプロパティのUnion typeを作ることはできません。

$NonMaybeType

$NonMaybeTypeTに指定したMaybe Typeをnullundefinedを許可しない元の型に戻す型です。

# Try Flow

type A = ?string;

var a1: A = 'hoge';
var a2: A = null;
var b1: $NonMaybeType<A> = 'fuga';
var b2: $NonMaybeType<A> = null; // Error !!

$Shape

$ShapeTに指定した型の部分型であり、Objectとは違いTと一致しないプロパティを許容しない型になります。

# Try Flow

type A = {a: number, b: string, c: boolean};

var a: $Shape<A> = {a: 1, b: 'hoge', c: true};
var b: $Shape<A> = {a: 1};
var c: $Shape<A> = {b: 'hoge'};
var d: $Shape<A> = {c: true};
var e: $Shape<A> = {d: 1.1}; // Error !!

ただし、Union type、Intersection type、後述のDiff typeと組み合わせると、それぞれで許容される型が変わります。

# Try Flow

type A = {a: number, b: string};
type B = {b: string, c: boolean};

// Unon type
var a00: $Shape<A | B> = {};
var a01: $Shape<A | B> = {a: 1}; // Error !!
var a02: $Shape<A | B> = {b: 'hoge'};
var a03: $Shape<A | B> = {c: true}; // Error !!
var a04: $Shape<A | B> = {d: 1.1}; // Error !!
var a05: $Shape<A | B> = {a: 1, b: 'hoge'}; // Error !!
var a06: $Shape<A | B> = {a: 1, c: true}; // Error !!
var a07: $Shape<A | B> = {a: 1, d: 1.1}; // Error !!
var a08: $Shape<A | B> = {b: 'hoge', c: true}; // Error !!
var a09: $Shape<A | B> = {b: 'hoge', d: 1.1}; // Error !!
var a10: $Shape<A | B> = {a: 1, b: 'hoge', c: true}; // Error !!
var a11: $Shape<A | B> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var a12: $Shape<A | B> = {a: 1, c: true, d: 1.1}; // Error !!
var a13: $Shape<A | B> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

// Intersection type
var b00: $Shape<A & B> = {};
var b01: $Shape<A & B> = {a: 1};
var b02: $Shape<A & B> = {b: 'hoge'};
var b03: $Shape<A & B> = {c: true};
var b04: $Shape<A & B> = {d: 1.1}; // Error !!
var b05: $Shape<A & B> = {a: 1, b: 'hoge'};
var b06: $Shape<A & B> = {a: 1, c: true};
var b07: $Shape<A & B> = {a: 1, d: 1.1}; // Error !!
var b08: $Shape<A & B> = {b: 'hoge', c: true};
var b09: $Shape<A & B> = {b: 'hoge', d: 1.1}; // Error !!
var b10: $Shape<A & B> = {a: 1, b: 'hoge', c: true};
var b11: $Shape<A & B> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var b12: $Shape<A & B> = {a: 1, c: true, d: 1.1}; // Error !!
var b13: $Shape<A & B> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

// Diff type
var c00: $Shape<$Diff<A, B>> = {};
var c01: $Shape<$Diff<A, B>> = {a: 1}; // Error !!
var c02: $Shape<$Diff<A, B>> = {b: 'hoge'}; // Error !!
var c03: $Shape<$Diff<A, B>> = {c: true}; // Error !!
var c04: $Shape<$Diff<A, B>> = {d: 1.1}; // Error !!
var c05: $Shape<$Diff<A, B>> = {a: 1, b: 'hoge'}; // Error !!
var c06: $Shape<$Diff<A, B>> = {a: 1, c: true}; // Error !!
var c07: $Shape<$Diff<A, B>> = {a: 1, d: 1.1}; // Error !!
var c08: $Shape<$Diff<A, B>> = {b: 'hoge', c: true}; // Error !!
var c09: $Shape<$Diff<A, B>> = {b: 'hoge', d: 1.1}; // Error !!
var c10: $Shape<$Diff<A, B>> = {a: 1, b: 'hoge', c: true}; // Error !!
var c11: $Shape<$Diff<A, B>> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var c12: $Shape<$Diff<A, B>> = {a: 1, c: true, d: 1.1}; // Error !!
var c13: $Shape<$Diff<A, B>> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

Union typeの場合は空オブジェクトとABの共通部分の部分型に。
Intersection typeの場合は空オブジェクトとABの部分型に。
Diff typeの場合は空オブジェクトのみを許容する形になります。

正直、Diff typeのパターンはバグだと思います。変数c01のパターンは許容されるべきでは・・・。

$Diff

https://flowtype.org/docs/utility-types.html#diffa-b

$DiffTからSを除外した型を許容する型(Diff type)になります。

# Try Flow

type A = {a: number, b: string};
type B = {b: string, c: boolean};

var a: $Diff<A, B> = {a: 1};
var c: $Diff<A, B> = {a: 1, b: 'hoge'};
var d: $Diff<A, B> = {a: 1, c: true};
var e: $Diff<A, B> = {b: 'hoge', c: true}; // Error !!

実際にどう使うかは悩ましいですが、$Diffがあることで追加だけでなく除外できるようになるため、型の表現力が格段に上がります。

$Keys | $Enum

https://flowtype.org/docs/utility-types.html#keyst

$KeysTに指定した型のキーのUnion typeになります。

# Try Flow

type A = {a: number, b: string, c: boolean};

var a: $Keys<A> = 'a';
var b: $Keys<A> = 'b';
var c: $Keys<A> = 'c';
var d: $Keys<A> = 'd'; // Error !!

この場合、$Keys<A>'a' | 'b' | 'c'と等価になります。

$Exact

$ExactTに指定した型の同型のみを許容する型になります。
v0.3.2からは構文が導入され{| |}で表現可能になっています。

# Try Flow

type A = {a: number, b: string, c: boolean};

var a: $Exact<A> = {a: 1, b: 'hoge', c: true};
var b: $Exact<A> = {a: 1, b: 'hoge'}; // Error !!
var c: $Exact<A> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

$Exact$Shapeと違い、Union type、Intersection type、Diff typeと組み合わせると代入不可能な型になります。

# Try Flow

type A = {a: number, b: string};
type B = {b: string, c: boolean};

// Unon type
var a00: $Exact<A | B> = {}; // Error !!
var a01: $Exact<A | B> = {a: 1}; // Error !!
var a02: $Exact<A | B> = {b: 'hoge'}; // Error !!
var a03: $Exact<A | B> = {c: true}; // Error !!
var a04: $Exact<A | B> = {d: 1.1}; // Error !!
var a05: $Exact<A | B> = {a: 1, b: 'hoge'}; // Error !!
var a06: $Exact<A | B> = {a: 1, c: true}; // Error !!
var a07: $Exact<A | B> = {a: 1, d: 1.1}; // Error !!
var a08: $Exact<A | B> = {b: 'hoge', c: true}; // Error !!
var a09: $Exact<A | B> = {b: 'hoge', d: 1.1}; // Error !!
var a10: $Exact<A | B> = {a: 1, b: 'hoge', c: true}; // Error !!
var a11: $Exact<A | B> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var a12: $Exact<A | B> = {a: 1, c: true, d: 1.1}; // Error !!
var a13: $Exact<A | B> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

// Intersection type
var b00: $Exact<A & B> = {}; // Error !!
var b01: $Exact<A & B> = {a: 1}; // Error !!
var b02: $Exact<A & B> = {b: 'hoge'}; // Error !!
var b03: $Exact<A & B> = {c: true}; // Error !!
var b04: $Exact<A & B> = {d: 1.1}; // Error !!
var b05: $Exact<A & B> = {a: 1, b: 'hoge'}; // Error !!
var b06: $Exact<A & B> = {a: 1, c: true}; // Error !!
var b07: $Exact<A & B> = {a: 1, d: 1.1}; // Error !!
var b08: $Exact<A & B> = {b: 'hoge', c: true};
var b09: $Exact<A & B> = {b: 'hoge', d: 1.1}; // Error !!
var b10: $Exact<A & B> = {a: 1, b: 'hoge', c: true};
var b11: $Exact<A & B> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var b12: $Exact<A & B> = {a: 1, c: true, d: 1.1}; // Error !!
var b13: $Exact<A & B> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

// Diff type
var c00: $Exact<$Diff<A, B>> = {}; // Error !!
var c01: $Exact<$Diff<A, B>> = {a: 1}; // Error !!
var c02: $Exact<$Diff<A, B>> = {b: 'hoge'}; // Error !!
var c03: $Exact<$Diff<A, B>> = {c: true}; // Error !!
var c04: $Exact<$Diff<A, B>> = {d: 1.1}; // Error !!
var c05: $Exact<$Diff<A, B>> = {a: 1, b: 'hoge'}; // Error !!
var c06: $Exact<$Diff<A, B>> = {a: 1, c: true}; // Error !!
var c07: $Exact<$Diff<A, B>> = {a: 1, d: 1.1}; // Error !!
var c08: $Exact<$Diff<A, B>> = {b: 'hoge', c: true}; // Error !!
var c09: $Exact<$Diff<A, B>> = {b: 'hoge', d: 1.1}; // Error !!
var c10: $Shape<$Diff<A, B>> = {a: 1, b: 'hoge', c: true}; // Error !!
var c11: $Exact<$Diff<A, B>> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var c12: $Exact<$Diff<A, B>> = {a: 1, c: true, d: 1.1}; // Error !!
var c13: $Exact<$Diff<A, B>> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

これはUnion typeの場合はAもしくはBを満たす型がなくなり。
Intersection type、Diff typeではExactがサポートされていないためエラーになります。
(Intersection typeではexact type. Type is incompatible with (unclassified use type: MakeExactT) any member of intersection typeというエラーが発生し、Diff typeではexact type. Unsupported exact typeが発生する)

また、Exact type同士でUnion type、Intersection type、Diff typeを作成すると上記までの結果と変わります。

# Try Flow

type A = {| a: number, b: string |};
type B = {| b: string, c: boolean |};

// Unon type
var a00: A | B = {}; // Error !!
var a01: A | B = {a: 1}; // Error !!
var a02: A | B = {b: 'hoge'}; // Error !!
var a03: A | B = {c: true}; // Error !!
var a04: A | B = {d: 1.1}; // Error !!
var a05: A | B = {a: 1, b: 'hoge'};
var a06: A | B = {a: 1, c: true}; // Error !!
var a07: A | B = {a: 1, d: 1.1}; // Error !!
var a08: A | B = {b: 'hoge', c: true};
var a09: A | B = {b: 'hoge', d: 1.1}; // Error !!
var a10: A | B = {a: 1, b: 'hoge', c: true}; // Error !!
var a11: A | B = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var a12: A | B = {a: 1, c: true, d: 1.1}; // Error !!
var a13: A | B = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

// Intersection type
var b00: A & B = {}; // Error !!
var b01: A & B = {a: 1}; // Error !!
var b02: A & B = {b: 'hoge'}; // Error !!
var b03: A & B = {c: true}; // Error !!
var b04: A & B = {d: 1.1}; // Error !!
var b05: A & B = {a: 1, b: 'hoge'}; // Error !!
var b06: A & B = {a: 1, c: true}; // Error !!
var b07: A & B = {a: 1, d: 1.1}; // Error !!
var b08: A & B = {b: 'hoge', c: true}; // Error !!
var b09: A & B = {b: 'hoge', d: 1.1}; // Error !!
var b10: A & B = {a: 1, b: 'hoge', c: true}; // Error !!
var b11: A & B = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var b12: A & B = {a: 1, c: true, d: 1.1}; // Error !!
var b13: A & B = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

// Diff type
var c00: $Diff<A, B> = {}; // Error !!
var c01: $Diff<A, B> = {a: 1}; // Error !!
var c02: $Diff<A, B> = {b: 'hoge'}; // Error !!
var c03: $Diff<A, B> = {c: true}; // Error !!
var c04: $Diff<A, B> = {d: 1.1}; // Error !!
var c05: $Diff<A, B> = {a: 1, b: 'hoge'}; // Error !!
var c06: $Diff<A, B> = {a: 1, c: true}; // Error !!
var c07: $Diff<A, B> = {a: 1, d: 1.1}; // Error !!
var c08: $Diff<A, B> = {b: 'hoge', c: true}; // Error !!
var c09: $Diff<A, B> = {b: 'hoge', d: 1.1}; // Error !!
var c10: $Diff<A, B> = {a: 1, b: 'hoge', c: true}; // Error !!
var c11: $Diff<A, B> = {a: 1, b: 'hoge', d: 1.1}; // Error !!
var c12: $Diff<A, B> = {a: 1, c: true, d: 1.1}; // Error !!
var c13: $Diff<A, B> = {a: 1, b: 'hoge', c: true, d: 1.1}; // Error !!

Union typeではExact typeのAもしくはBと一致するので変数a5a8でエラーが出ません。
しかし、Intersection typeではAB共にExact typeのため、それぞれに定義されたプロパティを持つことができません。
そのためAから見るとBのプロパティcが余分になるためエラーとなり、Bから見たときは逆にプロパティaが余分になりエラーとなってしまいます。
Diff typeの場合はよくわかりませんが、Exact typeなので除くということが出来ないということだと思います。(たぶん)

$Exports<'M'>

$Exportsimportrequireで読み込まれるモジュールの型です。

# Try Flow

declare module M {
  declare class Hoge {}
  declare class Fuga {}
}

var a: $Exports<'M'> = require('M');

var a1: string = a.Hoge; // Error !!
var a2: string = a.Fuga; // Error !!
var a3: string = a.Piyo;

※ 実際にはMというモジュールは作らないとrequireでもエラーは発生します

少し挙動は特殊で、モジュールに定義されたHogeFugaは定義通りの型となり、違う型の変数に代入するとエラーになります。
しかし、未定義のPiyoany扱いで読み込まれるためエラーにはなりません。
このあたりの挙動は

  • /* @flow */付きのファイルのモジュールを読み込む
  • /* @flow */のないファイルのモジュール読み込む
  • declare moduleで定義されたモジュールを読み込む
  • 存在しないファイルを読み込む

の組み合わせ方で挙動が変わります。

まぁ、普通にimportを使っていれば$Exportsは使わないので、そのあたりの検証は割愛します。

$Abstract

https://flowtype.org/docs/utility-types.html#abstractt

$AbstractT型を抽象型に変換する型です。

# Try Flow

class A<T> {
  static a: T;
}
class B<T> {
  static a: $Abstract<T>;
}

A.a.b.c;
B.a.b.c; // Error !

上記のように型パラメーターを取るクラスで、staticなプロパティに受け取った型パラメーターを指定すると、パラメータを渡さずに呼び出したさいにそのプロパティはany(もしくはunknown)として判定されてしまいます。
しかし、それでは困るといった場合に直接のアクセスを防ぐために$Abstractを使用して、アクセスできないように抽象型に変換します。

https://github.com/facebook/flow/blob/master/lib/react.js#L19

これはReactの型定義を書くときに利用するのが一般的な使い方のようです。

おわりに

magic typeはおもしろいですね!!
最近、JSらしいコードを残しながら型安全を目指すにはどうすれば良いんだろ・・・?と思っていたのですが、$Either$All$Diff$Shape$ExactObjectを操作する型の充実ぶりを見るとこれだ!!という感じがあります。
表現力高いし、推論もなかなか強く、JSのエコシステムにそのまま乗れて便利なので、もっとflowtype人口が増えてくれればいいのに。

--

その2に続く。