LoginSignup
8
6

More than 5 years have passed since last update.

Flowtypeにおける$ExactとObject Rest Spread

Last updated at Posted at 2017-06-29

前提

(※ 以下の話は現行バージョンのFlow(flowtype) = version 0.49 での話)

Flow (flowtype) において、他にプロパティをもたないオブジェクトを定義するのに、$ExactとよばれるUtility Typeが存在する。

type A = { foo: number, bar: string };
const a1: A = { foo: 1 }; // NG
const a2: A = { foo: 1, bar: 'text' }; // OK
const a3: A = { foo: 1, bar: 'text', baz: true }; // OK

type B = $Exact<A>;
const b1: B = { foo: 1 }; // NG
const b2: B = { foo: 1, bar: 'text' }; // OK
const b3: B = { foo: 1, bar: 'text', baz: true }; // NG

なお、$Exactのかわりに {| foo: number, bar: string |} のような指定もできる。

Object Rest/Spreadにおける問題

ここで、Reduxのreducerをコーディングするときなど、よくObject Rest/Spreadという仕組みを使って書くことが多い。Object rest/spread自体は現在stage 3であるが、babelを利用する環境ではpluginで組み込むことが多い。

しかし、Exact typeを使っていると、ここで以下のようなエラーが出てしまう

type B = $Exact<{ foo: number, bar: string }>;
const b1: B = { foo: 1, bar: 'text1' };
const b2: B = { ...b1, bar: 'text2' }; // NG
/* 
=>
  const b2: B = { ...b1, bar: 'text2' };
                 ^ object literal. Inexact type is incompatible with exact type
*/

Exact object typeが使えないと、たとえばReduxにおけるreducerのコードは、たとえば以下のようなtypoを見逃してしまう可能性が高い。

type State = { foo: number, bar: string }; // not exact type

const initState: State = { foo: 1, bar: '' };

// this typo can not be checked by flowtype
function reducer(state: State = initState, action: Action) {
  if (action.type === 'MODIFY_BAR') {
    return { ...state, barr: '<= prop name typo is here!!!' };
  }
  return state;
}

回避策とその限界

これについてはすでにissueにて議論があり、以下のような回避方法を提案している人もいる。

type Exact<T> = T & $Shape<T>;

type B = Exact<{ foo: number, bar: string }>;
const b1: B = { foo: 1, bar: 'text1' };
const b2: B = { ...b1, bar: 'text2' }; // OK
const b3: B = { foo: 1, bar: 'text3', baz: false }; // NG

なんとなく回避策でもよさそうな気がするが、もうすこし複雑なType、たとえばUnionを使った場合は、どうなるか。

// using normal exact type notation
type C1 = $Exact<{
  version: '1',
  foo: number,
  bar: string,
}> | $Exact<{
  version: '2',
  foo: number,
  bar: string,
  baz: boolean,
}>;

const c11: C1 = { version: '1', foo: 1, bar: 'text' }; // OK
const c12: C1 = { version: '1', foo: 1, bar: 'text', baz: true }; // NG
const c13: C1 = { version: '2', foo: 1, bar: 'text', baz: true }; // OK
const c14: C2 = { version: '2', foo: 1, bar: 'text', baz: true, qux: 'hello' }; // NG


// using custom exact
type Exact<T> = T & $Shape<T>;

type C2 = Exact<{
  version: '1',
  foo: number,
  bar: string,
}> | Exact<{
  version: '2',
  foo: number,
  bar: string,
  baz: boolean,
}>;

const c21: C2 = { version: '1', foo: 1, bar: 'text' }; // OK
const c22: C2 = { version: '1', foo: 1, bar: 'text', baz: true }; // NG
const c23: C2 = { version: '2', foo: 1, bar: 'text', baz: true }; // OK
const c24: C2 = { version: '2', foo: 1, bar: 'text', baz: true, qux: 'hello' }; // NG

ここまで、上記のコードではC1(標準のExact Typeを利用)とC2(回避策であるカスタムのExact Typeを利用)とで同じ結果になっているが、以下のコードでつまづく。

// OK
function baz1(c1: C1): boolean {
  if (c1.version === '2') {
    return c1.baz;
  }
  return false;
}

// NG
function baz2(c2: C2): boolean {
  if (c2.version === '2') {
    return c2.baz;
  }
  return false;
}
/*
=>
    return c2.baz;
               ^ property `baz`. Property cannot be accessed on any member of intersection type
*/

 つまり、上記の回避方法のカスタムExactでは、Union typeでのプロパティアクセス、特にtype refinementが絡まってくるケースには使えないようである。

上記問題の回避策

見つかっていない。union type の利用が不可避なstate設計では、Exact typeを諦めるのが正しいのだろうか?

8
6
1

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
8
6