Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is type {} (non-nullish values) assignable to subtypes like { [key: string]: string }?

Tags:

typescript

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?

like image 204
Maggyero Avatar asked Oct 30 '25 03:10

Maggyero


1 Answers

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.

like image 156
jcalz Avatar answered Nov 01 '25 19:11

jcalz