JavaScript
flow

flowtypeのUtility Typeについて その3

この記事ではflowtypeのv0.50.0からv0.59.0にかけて追加されたUtility Typeについて記載しています。
それ以前のバージョンで追加されたUtility Typeについては下記を参照してください。

$ElementType

https://flow.org/en/docs/types/utilities/#toc-elementtype

$ElementTypeTObjectを指定して、stringにそのキーの文字列を渡すと対応した値の型を返します。
これだけ聞くと$PropertyTypeと同じように感じますが、$PropertyTypeではキーにString Literalのみの指定だったのが、$ElementTypeではキーにStringを指定できるため適用できる範囲が広くなりました。

# Try Flow

declare function getObjectValue<O: Object, K>(obj: O, key: K): $ElementType<O, K>;

type Obj = {
  a: number;
  b: string;
  c: boolean;
};
declare var obj: Obj;
let key = 'a';
let value = getObjectValue(obj, key);
value.toFixed();
value.replace('', ''); // Error !!

これは恐らくバグだとは思うのですが、v0.50.0では引数にこの型を利用するとemptyを返します。(emptyはflow.jsのdumpTypesで出力される推論できなかったときに発生する型)

# Try Flow

declare function getObjectValueFunction<O: Object, K>(obj: O, key: K): (v: $ElementType<O, K>) => void;

type Obj = {
  a: number;
  b: string;
  c: boolean;
};
declare var obj: Obj;
let key = 'a';
let fn = getObjectValueFunction(obj, key);
fn(1);
fn('b');      // Not Error !!
fn(true);     // Not Error !!
fn([]);       // Not Error !!
fn(() => {}); // Not Error !!
fn(null);     // Not Error !!
fn(void 0);   // Not Error !!

そのため、例えばObjectに値をセットするような関数などでは型で縛ることはできません。

とりあえずは値を取得するときに利用するしかないと思いますが、型の表現の幅はかなり広がるのでおもしろいUtility Typeです。

$Values

https://flow.org/en/docs/types/utilities/#toc-values

$ValuesTに指定したObjectの値のUnion Typeを返します。
ひらたく言うと$Keysの値を返す版ですね。

# Flow Type

type Obj = {
  a: number;
  b: string;
  c: boolean;
};
declare var obj: Obj;
declare var value1: $Values<Obj>;

let value2: number | string | boolean = value1;
let value3: number | string = value1; // Error !!

使い所としてはObject.valuesぐらいだとは思います。

$Rest

https://flow.org/en/docs/types/utilities/#toc-rest

$RestはObject Rest Propertiesを表現する型です。

let t1 = {a: 1, b: 'foo', t2: true};
let {t2, ...rest} = t1;

というようなコードにおけるrestの型を表現する型になります。

# Flow Type

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

declare var o: $Rest<Obj, {| c: boolean |}>;
(o: {| |}); // Error: property not found `a`, `b`
(o: {| a: number, b: string |});

基本的に$Restは上記のようにExact Typeを使うと想定通りの動きします。
しかし、T2Objectを指定すると

# Flow Type

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

declare var o1: $Rest<Obj, {c: boolean}>;
(o1: {| |}); // Error: property not found `a`, `b`, `c`
(o1: {| a: number, b: string, c: boolean |}); // Error: undefined. This type is incompatible with `number`, `string`, 'boolean'
(o1: {| a?: number, b?: string, c?: boolean |});

declare var o2: $Rest<Obj, {}>;
(o2: {| |}); // Error: property not found `a`, `b`, `c`
(o2: {| a: number, b: string, c: boolean |}); // Error: undefined. This type is incompatible with `number`, `string`, 'boolean'
(o2: {| a?: number, b?: string, c?: boolean |});

というようにT2の型に関わらず、全てのプロパティーがOptionalになります。
(一応、ドキュメントにはこの理由も記載されているのですが、ちゃんと理解できていません。Object型は全てのObjectの部分型であるため、T1のすべてのプロパティがT2に存在する可能性があり$Restの返す型のプロパティはすべてOptionalになる?でも、それならo1のように指定した場合はプロパティcが存在することは確定するので、プロパティco1から除外されるべきだし・・・と納得ができていません)

また、同じような挙動をする$Diffでは、$Restと違いT2ObjectでもExact Typeでも同じように動作します。

$Call

$Callは関数の戻り値型を返す型です。

# Try Flow

type Fn1 = () => number;
declare var call1: $Call<Fn1>;
call1.toFixed();
call1.replace('', ''); // Error

type Fn2 = <A, B>(a: A, b: B) => B;
declare var call2: $Call<Fn2, number, string>;
call2.toFixed(); // Error
call2.replace('', '');

上記のようにFnに関数の型を指定し、続く型に引数の型を渡すことで戻り値の型を取得することができます。

$Callの説明として足りていなかったので別記事にて詳細を記載しました。

$CharSet

$CharSetTに指定した文字集合を表した型になります。
つまり、

# Try Flow

("a": $CharSet<"ab">);
("b": $CharSet<"ab">);
("ab": $CharSet<"ab">);
("ba": $CharSet<"ab">);
("aaaa": $CharSet<"ab">); // Error
("c": $CharSet<"ab">); // Error
("ac": $CharSet<"ab">); // Error

上記のようにTで指定した文字が存在する文字列(存在しない文字は含まず、重複も許容しない)型になります。
文字を利用したフラグ管理などを使っていれば利用できそうではありますが、コマンドライン引数のフラグのように外部から入力された値には使えないため、かなり利用用途の低そうな型です。

$Compose、$ComposeReverse

$ComposeはHoC(Higher-order Components)を実現する関数の型になります。
つまり、関数を合成する関数の型で、$Composeは引数の後ろから前に呼び出されていく関数になり、$ComposeReverseはその逆になります。

# Try Flow

declare var compose: $Compose;

compose(
  s => s.replace('', ''),
  n => n.toString()
)(42);

しかし、上記のコードを実行するとcompose関数の定義がない状態になるため実行時エラーが発生します。
最近のReact事情はさっぱりなのですが、これは一体どこで使えばよいのでしょうね。

$ReadOnly

https://flow.org/en/docs/types/utilities/#toc-read-only-obj

$ReadOnlyTに指定したObjectを読み込みのみに可能なObjectを表す型です。
$ReadOnlyArrayが出た時にゴリ押しでReadOnlyなObject型を定義していましたが、これよりスマートに記述することができるようになりました。

# Try Flow

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

declare var obj: $ReadOnly<Obj>;
obj.a = 1; // Error

$ReadOnlyArrayと同様に書き込みがされないことを担保できるようになるので、あちらこちらで使うことができそうですね。

おわりに

という訳でUtility Typeのまとめ その3でした。
ここが間違っている、例示が悪いなどあれば、ぜひご指摘ください。