前提
(※ 以下の話は現行バージョンの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を諦めるのが正しいのだろうか?