Closed andersk closed 1 year ago
This isn’t specific to {}
and also happens for e.g. { toString(): string }
(which clearly should be assignable to object
). It’s a known soundness hole with object literal types.
@fatcerberus Is it clear that { toString(): string }
should be assignable to object
? Seems to me that "s"
is a great example of a { toString(): string }
that should not be assignable to object
.
If this is a known soundness hole, do you happen to have a reference where one can learn about these known soundness holes? The only ones I’ve found in the documentation are function parameter bivariance and optional/rest parameters; I’m aware of many others but I haven’t seen them documented, so I have no idea how to tell which ones are deliberate.
If { toString(): string }
is not assignable to object
, then what is[^1]? TS generally shies away from special cases like "object literal type is assignable to object
unless it's also assignable from a primitive"; the general rules in play are that any value which is coercible to the object literal type is also assignable to it, and all object literal types are assignable to object
, which are easy to reason about, even if they unfortunately lead to a soundness hole in this case.
I’m aware of many others but I haven’t seen them documented, so I have no idea how to tell which ones are deliberate.
It's not super ideal but the maintainers have said in the past that the issue tracker is considered part of the documentation w.r.t. small nuances like this.
[^1]: This isn't just me trying to be difficult. A more subtle case would be, e.g. { valueOf(): number, x?: string, y?: number }
.
Primitive numbers are assignable to it, but it would be super confusing if such a type wasn't assignable to object
, since that's what it's probably going to be 99.9% of the time. TS tries to be pragmatic about this kind of thing.
On the above note re: documentation, I was almost positive this exact issue had been reported and closed working-as-intended before, but I can't find it now.
FWIW I think it was a mistake for primitives to be assignable to object types at all, but alas, we're stuck with it now.
If
{ toString(): string }
is not assignable toobject
, then what is?
Is that question fundamentally any different from “if { valueOf(): boolean }
is not assignable to boolean
, then what is?”
Values that should be assignable to boolean
include true
and false
. Values that should be assignable to object
include object literal values like {x: "s", y: 1}
(as values, not as types), some other literals like ["arrays"]
and /^regexps$/
, and class instances like new Set()
. One should not expect an arbitrary member of a structural type to be assignable to object
any more than one should expect it to be assignable to boolean
. (That’s what unknown
is for.)
An even more direct argument is that any structural type can be subclassed with a call signature inhabited only by functions, for which typeof
returns "function"
, not "object"
.
No structural type is assignable to a primitive; all structural types are assignable to object
. TS is very consistent about that; unsoundness arises because primitives are assignable to some structural types. This IMO was a design mistake, but I have enough experience following issues here to know it’s not likely to change now.
Unless you have some affiliation with the TypeScript team that GitHub isn’t showing me, that’s for the TypeScript team to decide
Like I said, this exact issue has been reported before and declared as by design; I just can’t find the dupe at the moment. It’s already been decided and I’m just passing the message along.
No structural type is assignable to a primitive; all structural types are assignable to
object
. TS is very consistent about that;
I understand this is the current behavior, and you think it’s consistent and the problem is elsewhere, but what I’m saying that it’s not consistent.
If it were only a question about whether a primitive string should be considered a member of {}
, we could go either way about where the problem is. But consider this variant of my test with a function instead of a string. It similarly compiles with no errors and breaks at runtime:
let a: {(): void} = () => {};
let b: {} = a;
let c: object = b;
let d: number = typeof c === "object" ? 0 : c;
console.log(d.toFixed()); // runtime crash
I don’t think one can argue that () => {}
should not inhabit {(): void}
, or that {(): void}
should not be a subtype of {}
.
Hmm, c
is being narrowed to never
in the false branch of that ternary. Even without the unsoundness mentioned in the OP (i.e. non-transitive assignability), that's wrong -- functions are directly assignable to object
, and their runtime typeof
is "function"
.
Allowing { }
to assign to object
is allowed because object
is "relatively" new and there's just too much code out there that still uses { }
to mean object
. Maybe in a very long time we can close this hole, but now right now for sure.
unsoundness arises because primitives are assignable to some structural types.
Disagree. https://github.com/microsoft/TypeScript/issues/48988#issuecomment-1119793271
Also please don't snipe at my friendly triage helpers 😉
My apologies if that came across as sniping. I have deleted that part of my comment.
Disagree. https://github.com/microsoft/TypeScript/issues/48988#issuecomment-1119793271
To be fair, that proof only holds because TS treats object types as “object-coercible” rather than (what I think people generally expect) “non-primitive object of shape”. Which is fine but still interesting given TS’s general stance on automatic coercion. {}
notwithstanding, if I ask for something of type { … }
I generally don’t expect to receive primitives just because they happen to be object-coercible to something compatible with that type (luckily this is rarely an issue in practice due to weak type checks etc.)
@RyanCavanaugh Point I was trying to get at above was: it is technically unsound for any object type which is assignable from a primitive to be assignable to object
. IOW this is not specific to {}
and is a more general soundness hole induced by TS allowing for object coercion in the type system.
Regardless of what people expect, it is fundamentally not possible for { … }
to represent only object
types, given that call signatures exist and are only satisfied by function
types.
If the unsound coercion from { … }
to object
could be eventually removed, there would be no soundness issue with coercing primitives to { … }
as far as I can see.
That’s not true - functions are in the domain of object
by design (because they are objects). It’s probably a bug that typeof foo === "object"
is considered exhaustive.
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
Bug Report
🔎 Search Terms
coercion, object, typeof, unsound
🕗 Version & Regression Information
object
and unsoundness(I am of course aware that TypeScript has deliberately chosen to allow certain areas of unsoundness, but this doesn’t look like one of those deliberate choices.)
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
No compile errors. TypeScript allows
b: {}
to be coerced toc: object
, and I’ve taken advantage of this incorrect coercion to store astring
in a variable typednumber
.🙂 Expected behavior
TypeScript should not allow coercion from
b: {}
toc: object
, because the contract ofc: object
has an extra requirement thatb: {}
does not, namely thattypeof c === "object"
.