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 asobjectis an intentional compatibility hole sinceobjectdidn'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 objects 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
stringtype also includes properties from the globalStringtype.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
lengthof a string is now a fundamentally different operation than accessing thelengthof 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
Xeven 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 produceneverinstead.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 & objectis obviouslynever.I'd also just weigh in that making
interfaceimplicitly extendobjectbut 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& objectinto atypedeclaration. 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.
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