microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.02k stars 12.49k forks source link

"{}" type issues when comapred to "Object"/"object" #48988

Closed dblachut-adb closed 2 years ago

dblachut-adb commented 2 years ago

Bug Report

🔎 Search Terms

"object" | "{}"

🕗 Version & Regression Information

⏯ Playground Link

Playground link with relevant code

💻 Code

  1. type A = {}
    const a0: A = undefined // error
    const a1: A = null // error
    const a2: A = 2 // ok
    const a3: A = 'hello world' //ok
    const a4: A = { foo: 'bar' } //ok
    const a5: A = () => undefined //ok
  2. const X = {} // X: {}
    type D = typeof X // D = {}
  3. const Y = new Object() // Y: Object
    type E = typeof X // E = {}
  4. interface F<T extends object = {}> {
    prop: T;
    }

🙁 Actual behavior

I have found at least few issues with {} type, all presented in playground and code samples above:

  1. Is undocumented (or I couldn't find it in docs) and confusing - looks like object literal but can have primitives assigned.
  2. Object literal has a {} type assumed while it should rather be Object/object.
  3. new Object has a {} type assumed while it should rather be Object/object.
  4. {} type can be assigned to more narrow object type, which should not be possible.

🙂 Expected behavior

{} type is documented, cannot be assigned to more narrow object type and is not assumed when used with typeof <some_object>.

MartinJohns commented 2 years ago

https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-all-types-assignable-to-empty-interfaces

More information to clear your confusion: https://mariusschulz.com/blog/the-object-type-in-typescript

dblachut-adb commented 2 years ago

Okay so {} is an empty interface and that is the root of all evil. :wink: Makes sense, but can also be a source of errors, for example:

const X = {} // X: {}
const variableWithSameTypeAsX: typeof X = 1;

// typescript allows comparison of "number" and "object" which does not make sense
if (variableWithSameTypeAsX == X) {

}

Playground link.

In other words: IMHO primitives shall not be assignable to empty interface.

MartinJohns commented 2 years ago

It's intentional. You basically shouldn't be using {}, unless you understood it and have a good reason for it. It means "any type, except undefined and null."

IMHO primitives shall not be assigned to empty interface.

You're entitled to your own opinion, but it's working as intended. :-) See #44874, specifically https://github.com/microsoft/TypeScript/issues/44874#issuecomment-874892210.

dblachut-adb commented 2 years ago

Well, I think it does not make sense because primitives can't have methods and properties, so maybe they shouldn't be assignable to any interface. Thanks for the link and explanation though!

(the length in string comes from autoboxing feature and you can't add any property to a primitive)

Josh-Cena commented 2 years ago

TypeScript doesn't really make the distinction between a primitive and its boxed value. string is a subtype of String, so if String extends {}, so will string.

dblachut-adb commented 2 years ago

Maybe such distinction should exist for the reasons I mentioned - you cannot have any properties added to a primitive.

So the only possible assignment of primitive to an interface (except empty one which accepts everything) is when this interface have properties defined in it's boxed prototype.

Boxed prototypes are well-defined so maybe there is a way to restrict primitive to interface assignment while still allowing such code:

interface L { length: number }
const X: L = "str";
MartinJohns commented 2 years ago

I'm sure the TypeScript team has thought about this the numerous times this issue came up. Like Ryan mentioned, it has implications on subtype reduction.

But I fail to see the general issue this causes. Can you provide w real example? Your earlier example where you mentioned the comparison between number and object is fine, because your variable typed {} can either store a number or an object.

dblachut-adb commented 2 years ago

The whole argument is about whether number and other primitives should be stored in variable typed {} (empty interface). Here is similar example:

const X = 1;
const Y = {};
const Z = {
  prop: true,
};

if (X == Y) { // ok

}

if (X == Z) { // error

}

Playground link

Typescript in general disallows comparing between two different types as it does not make sense and can be erroneous, but it is allowed when the object is empty. In general inconsistency and confusion should be avoided when possible. As you can see this case can be confusing not only to me which could be an argument to make a change in the current behavior.

Unfortunately I don't have any real example except the one provided (that I have came up with) and TBH it looks like I have been misusing {} as a shortcut to object type since it looks like object literal.

So in general I don't insist on changing it but IMHO if confusion and inconsistency can be avoided it definitely should.

Josh-Cena commented 2 years ago

I don't know if {} being different from all other object types is a design decision or an oversight, but it's more or less a feature now: people use it to represent any non-nullish value. There's no other type that represents the same thing.

dblachut-adb commented 2 years ago

Yeah I guess you are right, it cannot be simply changed.

MartinJohns commented 2 years ago

I don't see how it's being different. There only seems to be the wrong assuming that the type {} means it's an object. Neither does interface or type mean you're dealing with an object.

RyanCavanaugh commented 2 years ago

Here's a proof of why string is { }

You can break this proof at any single step, but it introduces really bad assymmetries without fixing any other problem than "some people misapprehended that { } is a synonym of object", and it's impossible to remove all misapprehensions from a system.

dblachut-adb commented 2 years ago

Question related: can you have a type in typescript which represents object without any properties? So that it would be possible to assign only empty object to that type.

MartinJohns commented 2 years ago

@dblachut-adb No, currently you can't define such a type in TypeScript. This would require #12936.