Type {} refers to non-nullish values; it does not refer to objects with no properties.
But I noticed this behaviour:
const x: {} = 0; // okay, since 0 is a non-nullish value
const y: { [key: string]: string } = 0; // error, since 0 is not a { [key: string]: string }
const z: { [key: string]: string } = x; // okay, unexpected
I expected TypeScript to throw an error for the z assignment, like it does for the y assignment. Yet it type checks. Is it a bug?
This is working as intended, although it does demonstrate that assignability is not transitive. That is, there are types A, B, and C such that A extends B and B extends C but not A extends C. Transitivity of assignability is often true, but because TypeScript's type system is not fully sound, there are places where transitivity fails. You've found one of them: number extends {} is true, and {} extends {[key: string]: string} is true, but number extends {[key: string]: string} is false. For other examples and discussions, see microsoft/TypeScript#42479 and microsoft/TypeScript#47331.
So
const x: {} = 0;
is allowed because {} has no known properties and therefore none of 0's apparent members conflict with it.
And
const y: { [key: string]: string } = 0; // error
is disallowed because not every apparent member of 0 is assignable to string. For example, the toFixed member has type (fractionDigits?: number | undefined) => string, which is not a string. So it fails to match the index signature.
But why is
const z: { [key: string]: string } = x; // okay
allowed?
Well, the anonymous empty object type {} is not an interface, and therefore it can get an implicit index signature. Interfaces do not get implicit index signatures, as described in microsoft/TypeScript#15300, and discussed in the design notes in microsoft/TypeScript#7059; interfaces are mentioned in opposition to object literal types, and from context, we can conclude that non-interface object types are object literal types. (Presumably, such types have syntax like an object literal, with a surrounding {⋯}, and object literal values are given such types as well.)
Anyway, from the handbook description of implicit index signatures:
An object literal type is assignable to a type with an index signature if all known properties in the object literal are assignable to that index signature.
Since x is of type {}, then {} is compared to { [key: string]: string }. The type {} has no known properties, so it is trivially assignable to the string index signature. And therefore the implicit index signature matches and the assignment succeeds:
const z: { [key: string]: string } = x; // okay
Again, there's an unsoundness here. Merely because an object type isn't known to conflict with an index signature, it doesn't mean there is no conflict. Missing properties are not necessarily undefined. This might be what you are getting at by saying that a variable of type {} isn't necessarily an empty object. It can have all kinds of properties, and these properties might conflict with any given index signature. Yes, this is unsafe. But these assignments are so convenient that it would be really annoying to use the "safe" version that continually errors, which is indeed why implicit index signatures were introduced in the first place, to stop annoying developers with safety rules that hampered productivity.
Not everyone agrees with such decisions, but we can at least be certain that, for the examples shown here, TypeScript is behaving exactly as intended. It's not a bug.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With