Object != {}

Topics: General
Nov 27, 2013 at 10:34 PM
I noticed this change as of 0.9.5:
var x: { [name: string]: string; };
x = {};    // works
x = new Object();   // error: Index signatures of type Object and { [name: string]: string; } are incompatible
Why are Object and {} not the same thing? I'm curious. I've noticed the types are generally assignable to one another.
var y: {} = new Object();    // works
var z: Object = {};    // works
Nov 27, 2013 at 10:53 PM
Edited Nov 27, 2013 at 10:53 PM
x = {};
This is only allowed because the assignment introduces a contextual type on the object literal (spec section 4.5):
When an object literal is contextually typed by a type that includes a string index signature of type T, the resulting type of the object literal includes a string index signature with the widened form of the best common type of T and the types of the properties declared in the object literal
The equivalent version using parentheses to prevent contextual typing gives you the same error as if { } were new Object()
x = ({});    // error
Nov 28, 2013 at 3:36 AM
I'm going to have to work harder to understand that bit.
Dec 1, 2013 at 3:58 PM
This comes down to the way TypeScript treats the members that every object inherits from the Object prototype. A string index signature defines a constraint that must be satisfied by all properties on a particular object (in your example they must all be of type string), but the members inherited from Object are excluded from this check (as index signatures would otherwise be pretty useless). Likewise, an object is permitted to redefine a member inherited from Object with a type that isn't a subtype of the inherited member's type. Indeed, members from Object technically aren't inherited, but rather are simply present if the object doesn't define its own members with the same name. These rules exist such that an object can define members with any name and type without error--too much code would break if TypeScript required every object to be a strict subtype of Object.

Now, in your example, the literal {} has no members (when those inherited from Object are excluded) and therefore satisfies the string indexer constraint. But the Object type does have the members of Object (stating the obvious here), and those members do not satisfy the string indexer constraint. However, when it comes to assignment compatibility, the literal {} is compatible with Object because it doesn't redefine any of Object's properties.
Dec 2, 2013 at 4:55 PM
Oh that actually makes sense. Thank you for the clear explanation.
Dec 2, 2013 at 8:09 PM
This explanation makes sense, but it still seems slightly odd that two expressions in JavaScript which are equivalent are considered different in TypeScript because of type inference.

Was the reasoning behind this change to allow people to more easily create simple object maps? Or is there more to it than that?
Dec 6, 2013 at 5:23 PM
Was the reasoning behind this change to allow people to more easily create simple object maps?
Exactly. The problem comes down to this:
var stringMap: { [s: string]: string; };
var numberMap: { [s: string]: number; };
var notMap = { };

stringMap = notMap; // You want this to be an error
numberMap = notMap; // You want this to be an error
stringMap = numberMap; // You want this to be an error
Seems simple enough, we'll make it so that types without matching index signatures aren't assignment compatible. But then when you want to actually initialize one of those map objects, you find that that error is a giant pain:
var stringMap: { [s: string]: string; } = {}; // ... error? {} doesn't have a string->string indexer
stringMap = <{[s: string]: string; }>{ }; // Please stop making me write all these type assertions :(
... but we still want all those error cases in the first example to still be errors, despite the fact that there's not really an easily-described difference between this snippet and the error cases above.

The solution was to have contextual typing apply the indexer when appropriate. This solves the initializer problem while still keeping the "should be an error" cases as errors. This also fixes some other cases, such as function invocation:
function makeZoo(animalLookup: { [s: string]: Animal; }) { /*... */ }
// Empty zoo
makeZoo({}); // OK
// Mistake
makeZoo(stringMap); // Error