将确切的子元素传递给函数中的不精确对象失败

时间:2018-08-12 10:02:09

标签: flowtype

我想将具有确切对象参数的对象传递给函数,而不必指定完全匹配项:

/* @flow */
type a = {|
    b: string,
    c: number,
    d: {| e: number, f: string |},
|}

interface props { b: string, d: { e: number } }
function foo(x: props) {
  console.log(`${x.b}: ${x.d.e}`);
}

let var_a: a = {
  b: 'b',
  c: 0,
  d: { e: 1, f: 'f' }
};

foo(var_a)

不幸的是,流量0.78.0给出:

19: foo(var_a)
        ^ Cannot call `foo` with `var_a` bound to `x` because inexact object type [1] is incompatible with exact object type [2] in property `d`.
    References:
    8: interface props { b: string, d: { e: number } }
                                       ^ [1]
    5:  d: {| e: number, f: string |},
           ^ [2]
19: foo(var_a)
        ^ Cannot call `foo` with `var_a` bound to `x` because property `f` is missing in object type [1] but exists in object type [2] in property `d`.
    References:
    8: interface props { b: string, d: { e: number } }
                                       ^ [1]
    5:  d: {| e: number, f: string |},
           ^ [2]

我也尝试使用类型代替接口:

type props = { b: string, d: { e: number } }

现在,可以通过将d指定为确切元素来轻松解决此问题:

type props = { b: string, d: {| e: number, f: string |} }

这很烦人,因为我想在函数中指定最少数量的参数,即f从未使用过foo参数,因此我认为这不是必需的

您可以在Try Flow here

中找到代码

1 个答案:

答案 0 :(得分:1)

我对此做了进一步的试验,并确定了为什么会发生这种情况,但是我不确定我对答案是否满意。

AFAICT,这仅是功能子类型的怪癖。

// @flow

const Clapper = (opts:{style:string}) => { console.log('clapping '+opts.style); };
const InvokeClapper = (fn:({})=>void) => { fn({}) };
InvokeClapper(Clapper); // fails :(

const Clapper2 = (opts:{}) => { console.log('clapping'); };
InvokeClapper(Clapper2); // works!
Clapper({}); // works!
Clapper2({}); // works!

const a = (b:{}) => 1;
a({});  // works
a({style:'string'}); // works

const a2 = (b:({a:number})=>void) => 1;
const a3 = (b:({|a:number|})=>void) => 1; // this and the above behave the same. why does it assume exact type for function param?
a2( (b:{a:number})=>{} ); // works
a2((b:{a:number,style:string})=>{}); // fails :(

const c2 = (b:({[string]:any})=>void) => 1;
c2((b:{a:number,style:string})=>{}); // works

const c3 = (b:(any)=>void) => 1;
c3((b:{a:number,style:string})=>{}); // works

const c4 = (b:({})=>void) => 1;
c4((b:{a:number,style:string})=>{}); // fails

// how can i say: a function that takes any object?
// or a function which takes any subtype of this object? any object more specific than { a:string, ... }

// this is the textbook example from Flow docs Width Subtyping page; very similar
const c5 = (b:{foo:string}) => 1;
c5({foo:"test", bar:42}); // works!

const c6 = (b:({foo:string})=>void) => 1;
// i guess this is a type=>type comparison, not a value=>type comparison
c6((x:{foo:string,bar:number})=>{}); // fails
c6((x:{foo:string})=>{}); // works

// this is one solution which seems acceptable but i am still confused why is necessary?
const c7 = (b:(x:{foo:string,[string]:any})=>void) => 1;
// what I want to express is: function b promises, for its x object parameter,
// to read/write only on x.foo. 
// or, if it attempted to read/write other keys, that should be a compile-time error.
c7((x:{foo:string,bar:number})=>{}); // works

// since exact match seems to do nothing here, i almost wish we could change its meaning,
// so that instead of forcing you to pass a variable with no additional keys,
// it instead forced the function b to not attempt to access or write to keys other than those in the exact match.
const c8 = (b:({|foo:string|})=>void) => 1;
c8((x:{foo:string,bar:number})=>{}); // fails
const altc8: ({foo:string})=>void = (x:{foo:string,bar:number})=>{}; // fails; but using lovely shorter syntax from docs

// reading chapter "Subsets & Subtypes" > "Subtypes of functions"
// it seems like, as in the doc example function f3,
// "The subtype must accept at least the same inputs as its parent, 
//  and must return at most the same outputs."
const c9 = (b:({foo:string,bar:number})=>number|void) => 1; //  this is the parent
c9((x:{foo:string})=>{return undefined;}); // works      // so this is the subtype

// i dislike this though.
// from my perspective, it shouldn't matter how many keys on the object in parameter 1.
// they should just both be considered an inexact object of any number of keys.
// while the parent considers it a sealed/exact object of just one key [that it cares about]. arrgh...

// going with solution c7 for now

请参阅: https://flow.org/en/docs/lang/subtypes/#toc-subtypes-of-functions

更新:

我后来发现下一个问题是,对象类型中的函数会自动被视为只读/协变量(即{ +yourFn: ()=>void }),而{[string]:any}映射类型会自动将所有键标记为读/写。因此,如果您的子类型包含函数,则它将被拒绝,因为函数在子类型中是只读的,而在父类型中是读/写的。

我通过在父级中联合泛型(作为映射类型的替代方案)并在函数调用时根据需要指定泛型参数来解决THAT问题。

ie。,而不是:

const fn: ({ a:number, [string]: any })=>void = (x:{ a:number, b:number })=>{};
fn({a:1, b:2});

我去了:

const fn2 = <V>(x:{a:number} & V)=>{};
fn2<{b:number}>({ a:1, b:2 });

因此,将问题延迟到调用为止。它仍然提供编译时检查。到目前为止运作良好。看来很厉害! Flow类型的系统比Java中的强大。真是个转折!