facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.08k stars 1.85k forks source link

Unsoundness with covariant type #8718

Open charlag opened 3 years ago

charlag commented 3 years ago

Flow version: 0.156.0 (at least since 0.135.0)

Expected behavior

Making type generic does not introduce unsoundness.

Actual behavior

Context

Our Mithril definitions use types which looks similar to these:

export interface Vnode<Attrs> {
    attrs: Attrs,
    children: ChildArray,
    dom: HTMLElement,
}

declare interface Mithril {
    <AttrsT>(
        component: Class<MComponent<AttrsT>>,
        attributes: AttrsT,
        children?: Children
    ): Vnode<any>;
}

interface MComponent<+Attrs> {
    /** Creates a view out of virtual elements. */
    view(vnode: Vnode<Attrs>): Children;
}

export type Child = Vnode<any> | string | number | boolean | null;
export type ChildArray = Array<Children>;
export type Children = Child | ChildArray;

declare var m: Mithril;

// --
// --
// --

type TestAttrs = {
    test: number,
}

class TestComponent implements MComponent<TestAttrs> {
    view(vnode: Vnode<TestAttrs>): Children {
        return String(vnode.attrs.test)
    }
}

function test(): Children {
    return m(TestComponent, {
        test: "c" // this should not be allowed
    })
}
dsainati1 commented 3 years ago

Yep, this is a bug. We should not allow you to make Attrs covariant in the definition of MComponent, since it appears in an input position in the interface's definition.

charlag commented 3 years ago

Ah, of course, it should be contravariant, thanks!

charlag commented 3 years ago

This might be another issue but even when I declare it as contravariant (interface MComponent<-Attrs>) it accepts things like

m(TestComponent, {})

but at least not

m(TestComponent, { another: 2 })
jamesisaac commented 3 years ago

Issue from your last message is likely due to Flow treating {} as an unsealed object. You can spread null to achieve an empty object without it being unsealed: { ...null }. See #7424