Can someone please explain why Second
type below is true
?
type First = object extends {} ? true : false // true
type Second = {} extends object ? true : false // true
Knowing that {}
is anything except null
and undefined
while object
is any non-primitive type, I understand why First
type is true
. But I can't understand why Second
type is also true
You're right that {} extends object
should be false, because not every {}
is an object
. Primitives like string
or number
or boolean
are assignable to {}
(which accepts any non-nullish value) but not to object
(which rejects primitives). So then why is it allowed?
The authoritative answer is contained in a comment on microsoft/TypeScript#56205 by the TS team dev lead:
Allowing
{}
to be used asobject
is an intentional compatibility hole sinceobject
didn't always exist, so people used{}
as the next-best thing.
So it's intentionally unsound so as not to break lots of real world code with the existence of object
.
More details can be found in microsoft/TypeScript#60582. If {}
were not assignable to object
then neither would {a: string}
. And then something that accepts object
s would suddenly reject most types and interfaces because maybe they're primitive? (Even though {a: string}
can't be primitive.) So then maybe TypeScript would need to aggressively analyze every interface to see if it overlaps with a primitive and then allow those which do not overlap to be assignable to object
, so {length: number, a: string}
is assignable to object
but {length: number}
isn't? As mentioned in another comment by the TS team dev lead:
The root problem is that the inconsistencies here can't be removed, they can just be moved. We say that this program is legal
function foo(s: string) { return s.length; }
But why is it legal? It's legal because the
string
type also includes properties from the globalString
type.Similarly, we allow you to write this program, for the same reason:
function foo<T extends { length: number }>(x: T) { return x.length; } // Legal call foo("hello world");
The proposal here implies breaking this consistency: You can no longer take some block of expressions and talk about them in a higher-order fashion if some of those expressions involve property access on primitives. Accessing the
length
of a string is now a fundamentally different operation than accessing thelength
of an array.This raises further problems, consider something like this
const p = "foo"; type X = (typeof p)["length"]; type GetLength<T> = T extends { length: infer U } : U ? never; type Y = GetLength<typeof p>;
Is the type
X
even legal? One argument says yes, because it's what you'd get if you wrotep.length
. Another argument says no, because this should be basically equivalent toGetLength
, which would now producenever
instead.Every place we produce or ingest a property name, you now have to think about whether that property name exists as part of an object syntax or doesn't. e.g. what's
keyof string
?This also implicitly breaks common "branding" patterns like
string & { isNormalized: true }
sincestring & object
is obviouslynever
.I'd also just weigh in that making
interface
implicitly extendobject
but nottype X = { }
seems extremely inconsistent and, arguably, counterintuitive? If anything I'd choose the opposite. You can imagine making it legal to writeinterface Foo extends object {
(and making that idiomatic in your codebase) but there's not as much syntactic clarity in having to jam in an& object
into atype
declaration. But either way it introduces "yet another thing you have to know", which isn't great from the perspective of keeping the language learnable.
So the issue is that there's a weird unsoundness around {}
and object
, and that none of the proposed ways to handle it have worked without introducing other, weirder, unsoundnesses.