Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types declared as an "interface" do not extend Record<PropertyKey, unknown> #42825

Closed
Seally opened this issue Feb 16, 2021 · 6 comments
Closed

Comments

@Seally
Copy link

Seally commented Feb 16, 2021

Bug Report

🔎 Search Terms

  • interface type
  • inconsistent interface type extends

🕗 Version & Regression Information

  • This is the behavior in every version I tried*, and I reviewed the FAQ for entries about missing index signatures and type assignment.

*Checked 4.1.3, 4.1.5, current beta, nightly, and some previous versions.

⏯ Playground Link

Playground link with relevant code

💻 Code

type TFoo = {
    type: string;
    value: number;
};

interface IFoo {
    type: string;
    value: number;
}

// Just to check if "interface-ness" is carried over through type assignment.
type ITFoo = IFoo;

interface AnyObject {
    [key: string]: unknown;
}

type AnyObjectType = {
    [key: string]: unknown;
};

type TypeTest = TFoo extends Record<PropertyKey, unknown> ? true : false;            // => true
type InterfaceTest = IFoo extends Record<PropertyKey, unknown> ? true : false;       // => false (expected 'true')
type InterfaceTypeTest = ITFoo extends Record<PropertyKey, unknown> ? true : false;  // => false (expected 'true') 

type TypeTest2 = TFoo extends { [key: string]: unknown } ? true : false;            // => true
type InterfaceTest2 = IFoo extends { [key: string]: unknown } ? true : false;       // => false (expected 'true')
type InterfaceTypeTest2 = ITFoo extends { [key: string]: unknown } ? true : false;  // => false (expected 'true') 

type TypeTest3 = TFoo extends AnyObject ? true : false;            // => true
type InterfaceTest3 = IFoo extends AnyObject ? true : false;       // => false (expected 'true')
type InterfaceTypeTest3 = ITFoo extends AnyObject ? true : false;  // => false (expected 'true') 

type TypeTest4 = TFoo extends AnyObjectType ? true : false;            // => true
type InterfaceTest4 = IFoo extends AnyObjectType ? true : false;       // => false (expected 'true')
type InterfaceTypeTest4 = ITFoo extends AnyObjectType ? true : false;  // => false (expected 'true') 

function checkObject(obj: Record<PropertyKey, unknown>) {}

const fooType: TFoo = {
    type: "misc",
    value: 0
};

const fooInterface: IFoo = {
    type: "number",
    value: 20
};

const fooInterfaceType: ITFoo = {
    type: "number",
    value: 30
};

checkObject(fooType);
checkObject(fooInterface); // type error
checkObject(fooInterfaceType); // type error

🙁 Actual behavior

InterfaceTest and InterfaceTypeTest (all variations) are assigned the type of false, while the various TypeTests all return true.

Additionally, fooInterface and fooInterfaceType fails type check when passed to the stub function checkObject() with the error code 2345 (missing index signature).

🙂 Expected behavior

TypeTest, InterfaceTest, and InterfaceTypeTest (all variations) should be assigned the type true, and the three usages of the function checkObject() passes type check.

My understanding of unknown is that every type extends unknown (like any) and unknown extends only unknown, so object types of any shape should extend Record<PropertyKey, unknown>. From this is why I think the correct result is that all 3 variants of Foo extends Record<PropertyKey, unknown>.

Also, even if that assumption is incorrect, the types TFoo and IFoo only differ in that they're declared as a type or an interface. I expect that this means that TFoo and IFoo should be treated identically by various type constraints.

I initially encountered this issue when I was trying to pass an object with an interface-declared type to a function in the Deno standard library (specifically assertObjectMatch() under "testing/asserts" which accepts two arguments of type Record<PropertyKey, unknown>).

@MartinJohns
Copy link
Contributor

MartinJohns commented Feb 16, 2021

Types declared with inferface are a possible target for declaration merging, so their properties are not fully known. This is not the case for type aliases.

See #41518.

@Seally
Copy link
Author

Seally commented Feb 17, 2021

@MartinJohns Good point, but why does it IFoo extend Record<PropertyKey, any>?

type TFoo = {
    type: string;
    value: number;
};

interface IFoo {
    type: string;
    value: number;
}

type TFooCheck = TFoo extends Record<PropertyKey, any> ? true : false; // => true
type IFooCheck = IFoo extends Record<PropertyKey, any> ? true : false; // => true

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Feb 17, 2021

Any object type is assignable to { [s: string]: any } whether they have an inferrable index signature or not.

@ansavchenco
Copy link

ansavchenco commented Feb 26, 2021

Could someone explain this in more detail?
Why does unknown make a difference for interfaces? I understand that interface is a subject to declaration merging but can't think of an example of declaration merging that makes IFoo not assignable to Record<PropertyKey, unknown>.

Yes, all properties of IFoo interface are not fully known, but everything those unknown properties could be is still a subset of all possible properties, which is what Record<PropertyKey, unknown> is. Why is IFoo not assignable to Record<PropertyKey, unknown> then? What am I missing?

type TFoo = {
    type: string;
    value: number;
};

interface IFoo {
    type: string;
    value: number;
}

type IFooCheckAny = IFoo extends Record<PropertyKey, any> ? true : false; // => true
type IFooCheckUnknown = IFoo extends Record<PropertyKey, unknown> ? true : false; // => false
type TFooCheckUnknown = TFoo extends Record<PropertyKey, unknown> ? true : false; // => true

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 2, 2021

Why is IFoo not assignable to Record<PropertyKey, unknown> then? What am I missing?

This is exactly the train of thought that lead to anything being assignable to Record<string, any>, but there was concern from users at the time that then there was no way to indicate that they only wanted to accept things with a declared index signature. This makes a lot of sense if you intend to write to the received object, since if you alias an IFoo by a Record<string, unknown> then you can trivially corrupt it through e.g. myAlias["type"] = 0

@jcalz
Copy link
Contributor

jcalz commented May 6, 2022

Crosslinking #15300

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants