You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Rethinking relationships between {} type, object, and primitivesΒ #60582
functiontakesObject(x: object){// This rightly is a TS error.if(x===0){console.error('This branch should be unreachable');}}consto: {}=0;// But this isn't, and should be.takesObject(o);
π Actual behavior
No error on takesObject(o). {} can be assigned to type object, despite {} meaning "all nonnullish values", whereas object means only JS object types.
π Expected behavior
Error on takesObject(o). {} is a wider type than object.
Additional information about the issue
This stems from a typescript-eslint investigation into making no-unnecessary-condition be more correct around possibly-falsy "object types", such as {}, or even { toFixed(): string} (to which number may be assigned). See typescript-eslint/typescript-eslint#10378. This also relates to previous (controversial) conversations about whether to flag the {} type with the linter, see, e.g. typescript-eslint/typescript-eslint#8700.
I'm wondering if this was simply an oversight in #49119, which aimed to fix soundness holes with {}?
Allowing {} to be used as object is an intentional compatibility hole since object didn't always exist, so people used {} as the next-best thing.
I read this new issue as a request to not do that. As in, now that TypeScript has had better handling around {} and object for a while (refs: #49119, #57735 (comment)). Over in typescript-eslint we've overhauled our lint rules around the empty object types to respect the new behaviors and best practices (typescript-eslint/typescript-eslint#8700 -> https://typescript-eslint.io/rules/no-empty-object-type). This hole is a sticking point. +1 from me on filling in the hole.
Reacted by Kirk Waiblinger, ExE Boss and Hans Brende
Personally, I'm of a "strict" interpretation: that any { ... } type coincidentally assignable to a non-object primitive should not be assignable to object. Only with object & should they be. Quick examples:
{}: as discussed
{ length: number }: because of string
{ toPrecision(): string } and { toPrecision(precision?: number): string }: because of number
This is probably tricky from an implementation perspective. But it's right.
So you're suggesting that this shouldn't be allowed:
let obj: { length: number } = { length: 10 };
const o: object = obj;
Simply because obj could also hypothetically be a string?
If we're unpacking this in detail, negative - your example should be allowed π But the reason why is control flow narrowing, which TS already does in other cases as well.
This should be disallowed, though.
letobj: {length: number}={length: 10};// ... lots of codeif(Math.random()>0.5){obj="not an object!"}// ... lots of codeconsto: object=obj;
Yes, that was the point of the comment - it was a counterexample for @JoshuaKGoldberg's original argument.
...and I'm saying I don't see how it's a counterexample at all? It's just an edge case that would need to be handled by the same sleight-of-hand that TS already includes in similar situations.
Regardless, this is perhaps an implementation detail consideration that's not worth spending too many more words going back and forth on before we even find out if this issue will be summarily closed as a duplicate or not π. So I'll leave it there for now to avoid noise in the issue conversation. But definitely an important detail to handle if this does get to the point of implementation! π
We would need some proposal that creates the desired behavior without unacceptable negative side effects (we've taken several bites at this apple before and not come up with much that was particularly appetizing). No one is going to turn on this behavior if it means every time they write
to get something that's properly assignable to object, since in practice that's the only kind of thing that can inhabit Person anyway.
IMO the right fix here isn't making { } un-assignable to object, it's making objectness an intrinsic property of writing { } syntax in the first place, and then figuring out how to work backwards to a way to talk about types where you accept a { length: number } and don't care if it's an Array or a string. Maybe that's just giving up and saying that you have to write { length: number } | string.
Or maybe that's some kind of syntax around how the global String type is declared, e.g.
which would create a type that didn't have intrinsic object-ness. Point being, you should not (generally) have to do extra work to opt in to the existing behavior as relates to being able to assume that types are object unless known to be primitive.
Reacted by Kirk Waiblinger, Josh Ghoulberg π», Som Shekhar Mukherjee and Andy Edwards
I am enthusiastically on board with considering how to make it so that { ... } types are objects! I only framed the issue in the way I did on the assumption that that would be a nonstarter. But I agree with every word in #60582 (comment) π
I guess, before thinking too deeply to reinvent the wheel, I would be pretty interested to know how far down the rabbit hole those of you on the TS team have gone when pondering this previously, and what undesirable edge cases might have arisen. Do you have references on this that I might look into?
My first thought is that the overarching model here would be that the runtime JS type, i.e. the result of x === null ? 'null' : typeof x, would additionally be tracked in TS's type system. If for niche usages a Structural<T> intrinsic type needs to be provided, it could return the exact same type but with any associated runtime type (and therefore, for example, Structural<BoxedNumber> would be more or less identical to Structural<number>, apart from possibly readonlyness).
Since I can't help it, I've started to think through some details and consequences of this, but, as mentioned, probably it's best to catch up to where you are first, before dropping walls of text here π
Excited to be having this conversation!
Reacted by Josh Ghoulberg π», ExE Boss and Hans Brende
changed the title [-]`{}` type should not be assignable to `object`[/-][+]Rethinking relationships between `{}` type, `object`, and primitives[/+]on Nov 28, 2024
Seems to me that anytime interface is declared, there should be an implicitextends object or & object tacked on.
The typescript built-in primitive types should simply automatically remove that modifier from their corresponding reference interfaces internally (I don't think users themselves need the ability to remove that modifier since we can't create new primitive types! There can only ever be the built-in primitives, right?)
Without an interface declaration however, it seems far more elegant to not assume anything. For example: type X = {} should not extend object automatically, in my opinion: it makes the inheritance hierarchy non-transitive.
However, I would also echo Kirk Waiblinger (@kirkwaiblinger)'s request for references to any discussion/pitfalls/undesirable edge cases that have already been covered on this topic: would love to read more into it.
I couldn't find any relevant links (unfortunately this isn't very searchable). The root problem is that the inconsistencies here can't be removed, they can just be moved. We say that this program is legal
functionfoo(s: string){returns.length;}
But why is it legal? It's legal because the string type also includes properties from the global String type.
Similarly, we allow you to write this program, for the same reason:
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 the length of an array.
This raises further problems, consider something like this
constp="foo";typeX=(typeofp)["length"];typeGetLength<T>=Textends{length: infer U} : U ? never;typeY=GetLength<typeofp>;
Is the type X even legal? One argument says yes, because it's what you'd get if you wrote p.length. Another argument says no, because this should be basically equivalent to GetLength, which would now produce never 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 } since string & object is obviously never.
I'd also just weigh in that making interface implicitly extend object but not type X = { } seems extremely inconsistent and, arguably, counterintuitive? If anything I'd choose the opposite. You can imagine making it legal to write interface 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 a type 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.
Other issues that became apparent when making an extremely rough prototype (baseline results here)
Today unknown can be freely decomposed to and from the union {} | null | undefined. This would have to instead be the union {} | null | undefined | string | boolean | number | symbol | bigint. Not a dealbreaker but it's another complication to deal with.
Many JSDoc comments use Number instead of number, so that would be breaking almost everywhere. We could reinterpret Number in that context (we already do for other types) but it's another inconsistency.
string would no longer be Iterable<>; arguably this is a good thing because iterating a string is almost never a good idea, but if you really did mean to write a for/of loop on some programmatic data, it's unclear what you would do instead.
I think it'd be super weird to have string not be { [index: number]: string }, but that's a straightforward consequence.
The definition of NonNullable would have to change. Unclear what the downstream implications of that would be or if that would be a dealbreaker since we can't change the lib types based on compiler settings. I'd have to think about it more.
There's some very real inconsistency around inference of destructuring parameters. Let's say you write
functionfoo({ length }){}foo("bar");
This is legal; if you look at the inferred type of foo it's
functionfoo(arg: {length: any}) : void
so the call would become illegal. IMO that's pretty weird, since if you had written the equivalent downleveling, you'd be fine. Maybe we could iterate through all the primitives and add them to the signature if they'd be compatible and overlappy with the parameter type, but that seems like spooky action at a distance.
Ryan Cavanaugh (@RyanCavanaugh) unless I'm mistaken, all of the polymorphic inconsistency issues as well as the branding pattern/opaque type-alias issues you mentioned only exist in the world where {} == object. Hence why I suggested that not be the case, but rather the other way around: {} equivalent to boolean | string | number | bigint | symbol | object, with {} & object syntax available when a reference type is actually required (in most cases though, it is probably not required... the only place in (for example) the standard library I can even think of where object is a required input is Object.create and Object.setPrototypeOf!)
So we come to the problem where the vast majority of functions do not need to assumeobject in their parameter types (i.e., types in contravariant positions), while the vast majority of custom datatypes are in fact inhabited by object instances.
I suggested interface implicitly extend object simply to make life easier in the latter regard, similar to class declarations, which of course should definitelyextends object implicitly (it may be anecdotal, but usually when I see interface declarations, they are built with a non-primitive type in mind, similar to a class). This suggestion was simply meant to target the only pain-point mentioned. However, if that would pose too much of an inconsistency, then allowing interface extends object syntax would absolutely be a viable alternative in my mind.
You also noted that { ... } & object wouldn't have much syntactic clarity. That sentiment is confusing to me since the intersection operator seems to be so idiomatic. We have {} & T to narrow to non-nullable types, so I'm not sure why narrowing to reference types wouldn't be in keeping with the existing landscape. However, one could certainly imagine an alternative syntax such as:
typeX=object{ ... }
if that would make things clearer! Again though, the number of places where a type signature will actually requireobject as an input to a function I would think is minimal (I'm anecdotally looking at the standard lib). So I don't think people would need to care about tacking on object very often. (But correct me if I'm wrong.) And doing it the other way would risk just as much, if not more, extra boilerplate, given that many types in contravariant positions would want to opt out of this object requirement.
Reacted by Matt Kantor, __Enderlord__ and snarbles2
Another possibility I'd like to throw out to reduce possible boilerplate issues (keeping in mind, as stated above, this is not necessary for the proposal to work--I'm just targeting the only pain-point mentioned): interface extends object is valid syntax, and interface extends object is the default extends clause, but only if there is no explicit extends clause. Users can opt out of this by explicitly stating interface extends unknown, or interface extends XYZ to inherit the behavior of interface XYZ.
Ryan Cavanaugh (@RyanCavanaugh) I really appreciate the info you've provided. This is still on my mind, and I'm hoping to be able to give it a deep think when I have time, and also take a closer look at your prototype. Hoping you don't mind if this issue stays open for a bit π
I find myself mostly agreeing with Hans Brende (@HansBrende) on this issue. However I do not think extending object should be the default in any type context.
As mentioned before there are too many existing uses of types in contravariant position where the restriction to object is not desired. This includes parameter types marked with interfaces. Having to manually opt out of an implicit extends object seems like a rather breaking change.
Besides that TS mostly considers types not as representations of the actual runtime structure, but as restrictions on it. From a more practical perspective, this means that types tell you what you can do with an object, not what it is. So the type {} and it's corresponding interface just tell you that there is no property you know how to interact with. Thus, it makes sense that these types would be something that can have properties (non-nullish), but not necessarily extend object. The same goes for
interfaceX{length: number;}
It just describes the ability to read and store a number in the length property, not that the containing object is non-primitive. Actually needing an non-primitive object is rare and mostly comes down to calling internal library functions with that requirement.
As such I think the ideal sollution is to keep all interfaces and object types possibly primitive and only add that requirement when & object or the proposed extends object is added. After all, being non-primitive is almost never (correctly) used in practice. Hans Brende (@HansBrende) already mentioned Object.create and Object.setPrototypeOf and I agree that I have barely seen other use cases for it.
To cover the problem of obviously non-primitive objects not being assignable to object, having (JS) object literals, classes and all things with call and construt signatures (you all seem to forget that functions also extend object) automatically extend object implicitly would not break anything and allow passing them to functions specifically demanding object. In other cases, one should not expect an actual object anyways and use type guards like typeof x === 'object' to be safe, which already does narrow to object | null, the most common appearance of the object type as far as I have seen.
In the rare cases one actually demands an argument or property to be/stay strictly non-primitive, it is always possible to add the & object or extends object explicitly. As the rare exception, this should be the explicit case.
To summarize, this would mean leaving almost everything the same except:
Have all object literals implicitly intersect object when checked as parameter, written onto a variables, set as property in another literal or used as expression in some other way (e.g. ({ a(this: object) {} }).a()).
Only consider object, never, any and things containing call or construt signatures assignable to object. All other types must intersect object to be used as such (this includes {} to come back to the motivating example of this issue).
Ideally, introduce extends object for interfaces as you suggested to avoid the need for another type with & object appended. Possibly add this to a lot of standard library interfaces for built-in types like Object or Number and so on.
Activity
jcalz commentedon Nov 25, 2024
duplicate #56205
JoshuaKGoldberg commentedon Nov 25, 2024
Even if it's a duplicate, given #56205 (comment):
I read this new issue as a request to not do that. As in, now that TypeScript has had better handling around
{}andobjectfor a while (refs: #49119, #57735 (comment)). Over in typescript-eslint we've overhauled our lint rules around the empty object types to respect the new behaviors and best practices (typescript-eslint/typescript-eslint#8700 -> https://typescript-eslint.io/rules/no-empty-object-type). This hole is a sticking point. +1 from me on filling in the hole.jcalz commentedon Nov 25, 2024
Is it just
{}? So would{a: string}still be assignable toobjector not? What about{length: number}?JoshuaKGoldberg commentedon Nov 25, 2024
Personally, I'm of a "strict" interpretation: that any
{ ... }type coincidentally assignable to a non-objectprimitive should not be assignable toobject. Only withobject &should they be. Quick examples:{}: as discussed{ length: number }: because ofstring{ toPrecision(): string }and{ toPrecision(precision?: number): string }: because ofnumberThis is probably tricky from an implementation perspective. But it's right.
[-]`{}` should not be assignable to `object`[/-][+]`{}` type should not be assignable to `object`[/+]fatcerberus commentedon Nov 25, 2024
Josh Ghoulberg π» (@JoshuaKGoldberg) So you're suggesting that this shouldn't be allowed:
Simply because
objcould also hypothetically be astring?kirkwaiblinger commentedon Nov 25, 2024
If we're unpacking this in detail, negative - your example should be allowed π But the reason why is control flow narrowing, which TS already does in other cases as well.
This should be disallowed, though.
This should also be disallowed.
Note that similar DevX tricks are applied around nullishness, see, for example,
fatcerberus commentedon Nov 25, 2024
Kirk Waiblinger (@kirkwaiblinger)
Yes, that was the point of the comment - it was a counterexample for Josh Ghoulberg π» (@JoshuaKGoldberg)'s original argument.
kirkwaiblinger commentedon Nov 25, 2024
Bruce Pascoe (@fatcerberus)
...and I'm saying I don't see how it's a counterexample at all? It's just an edge case that would need to be handled by the same sleight-of-hand that TS already includes in similar situations.
Regardless, this is perhaps an implementation detail consideration that's not worth spending too many more words going back and forth on before we even find out if this issue will be summarily closed as a duplicate or not π. So I'll leave it there for now to avoid noise in the issue conversation. But definitely an important detail to handle if this does get to the point of implementation! π
RyanCavanaugh commentedon Nov 26, 2024
We would need some proposal that creates the desired behavior without unacceptable negative side effects (we've taken several bites at this apple before and not come up with much that was particularly appetizing). No one is going to turn on this behavior if it means every time they write
they actually need to write
to get something that's properly assignable to
object, since in practice that's the only kind of thing that can inhabitPersonanyway.IMO the right fix here isn't making
{ }un-assignable toobject, it's makingobjectness an intrinsic property of writing{ }syntax in the first place, and then figuring out how to work backwards to a way to talk about types where you accept a{ length: number }and don't care if it's anArrayor astring. Maybe that's just giving up and saying that you have to write{ length: number } | string.Or maybe that's some kind of syntax around how the global
Stringtype is declared, e.g.which would create a type that didn't have intrinsic object-ness. Point being, you should not (generally) have to do extra work to opt in to the existing behavior as relates to being able to assume that types are
objectunless known to be primitive.kirkwaiblinger commentedon Nov 28, 2024
Ryan Cavanaugh (@RyanCavanaugh)
I am enthusiastically on board with considering how to make it so that
{ ... }types are objects! I only framed the issue in the way I did on the assumption that that would be a nonstarter. But I agree with every word in #60582 (comment) πI guess, before thinking too deeply to reinvent the wheel, I would be pretty interested to know how far down the rabbit hole those of you on the TS team have gone when pondering this previously, and what undesirable edge cases might have arisen. Do you have references on this that I might look into?
My first thought is that the overarching model here would be that the runtime JS type, i.e. the result of
x === null ? 'null' : typeof x, would additionally be tracked in TS's type system. If for niche usages aStructural<T>intrinsic type needs to be provided, it could return the exact same type but with any associated runtime type (and therefore, for example,Structural<BoxedNumber>would be more or less identical toStructural<number>, apart from possibly readonlyness).Since I can't help it, I've started to think through some details and consequences of this, but, as mentioned, probably it's best to catch up to where you are first, before dropping walls of text here π
Excited to be having this conversation!
[-]`{}` type should not be assignable to `object`[/-][+]Rethinking relationships between `{}` type, `object`, and primitives[/+]HansBrende commentedon Dec 2, 2024
Seems to me that anytime
interfaceis declared, there should be an implicitextends objector& objecttacked on.The typescript built-in primitive types should simply automatically remove that modifier from their corresponding reference interfaces internally (I don't think users themselves need the ability to remove that modifier since we can't create new primitive types! There can only ever be the built-in primitives, right?)
Without an
interfacedeclaration however, it seems far more elegant to not assume anything. For example:type X = {}should not extendobjectautomatically, in my opinion: it makes the inheritance hierarchy non-transitive.However, I would also echo Kirk Waiblinger (@kirkwaiblinger)'s request for references to any discussion/pitfalls/undesirable edge cases that have already been covered on this topic: would love to read more into it.
RyanCavanaugh commentedon Dec 2, 2024
I couldn't find any relevant links (unfortunately this isn't very searchable). The root problem is that the inconsistencies here can't be removed, they can just be moved. We say that this program is legal
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:
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
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.RyanCavanaugh commentedon Dec 2, 2024
Other issues that became apparent when making an extremely rough prototype (baseline results here)
Today
unknowncan be freely decomposed to and from the union{} | null | undefined. This would have to instead be the union{} | null | undefined | string | boolean | number | symbol | bigint. Not a dealbreaker but it's another complication to deal with.Many JSDoc comments use
Numberinstead ofnumber, so that would be breaking almost everywhere. We could reinterpretNumberin that context (we already do for other types) but it's another inconsistency.stringwould no longer beIterable<>; arguably this is a good thing because iterating a string is almost never a good idea, but if you really did mean to write afor/ofloop on some programmatic data, it's unclear what you would do instead.I think it'd be super weird to have
stringnot be{ [index: number]: string }, but that's a straightforward consequence.The definition of
NonNullablewould have to change. Unclear what the downstream implications of that would be or if that would be a dealbreaker since we can't change the lib types based on compiler settings. I'd have to think about it more.There's some very real inconsistency around inference of destructuring parameters. Let's say you write
This is legal; if you look at the inferred type of
fooit'sso the call would become illegal. IMO that's pretty weird, since if you had written the equivalent downleveling, you'd be fine. Maybe we could iterate through all the primitives and add them to the signature if they'd be compatible and overlappy with the parameter type, but that seems like spooky action at a distance.
HansBrende commentedon Dec 3, 2024
Ryan Cavanaugh (@RyanCavanaugh) unless I'm mistaken, all of the polymorphic inconsistency issues as well as the branding pattern/opaque type-alias issues you mentioned only exist in the world where
{} == object. Hence why I suggested that not be the case, but rather the other way around:{}equivalent toboolean | string | number | bigint | symbol | object, with{} & objectsyntax available when a reference type is actually required (in most cases though, it is probably not required... the only place in (for example) the standard library I can even think of whereobjectis a required input isObject.createandObject.setPrototypeOf!)So we come to the problem where the vast majority of functions do not need to assume
objectin their parameter types (i.e., types in contravariant positions), while the vast majority of custom datatypes are in fact inhabited byobjectinstances.I suggested
interfaceimplicitly extend object simply to make life easier in the latter regard, similar toclassdeclarations, which of course should definitelyextends objectimplicitly (it may be anecdotal, but usually when I see interface declarations, they are built with a non-primitive type in mind, similar to a class). This suggestion was simply meant to target the only pain-point mentioned. However, if that would pose too much of an inconsistency, then allowinginterface extends objectsyntax would absolutely be a viable alternative in my mind.You also noted that
{ ... } & objectwouldn't have much syntactic clarity. That sentiment is confusing to me since the intersection operator seems to be so idiomatic. We have{} & Tto narrow to non-nullable types, so I'm not sure why narrowing to reference types wouldn't be in keeping with the existing landscape. However, one could certainly imagine an alternative syntax such as:if that would make things clearer! Again though, the number of places where a type signature will actually require
objectas an input to a function I would think is minimal (I'm anecdotally looking at the standard lib). So I don't think people would need to care about tacking onobjectvery often. (But correct me if I'm wrong.) And doing it the other way would risk just as much, if not more, extra boilerplate, given that many types in contravariant positions would want to opt out of thisobjectrequirement.HansBrende commentedon Dec 3, 2024
Another possibility I'd like to throw out to reduce possible boilerplate issues (keeping in mind, as stated above, this is not necessary for the proposal to work--I'm just targeting the only pain-point mentioned):
interface extends objectis valid syntax, andinterface extends objectis the default extends clause, but only if there is no explicitextendsclause. Users can opt out of this by explicitly statinginterface extends unknown, orinterface extends XYZto inherit the behavior of interfaceXYZ.kirkwaiblinger commentedon Dec 17, 2024
Ryan Cavanaugh (@RyanCavanaugh) I really appreciate the info you've provided. This is still on my mind, and I'm hoping to be able to give it a deep think when I have time, and also take a closer look at your prototype. Hoping you don't mind if this issue stays open for a bit π
I-Enderlord-I commentedon Nov 23, 2025
I find myself mostly agreeing with Hans Brende (@HansBrende) on this issue. However I do not think extending
objectshould be the default in any type context.As mentioned before there are too many existing uses of types in contravariant position where the restriction to
objectis not desired. This includes parameter types marked with interfaces. Having to manually opt out of an implicitextends objectseems like a rather breaking change.Besides that TS mostly considers types not as representations of the actual runtime structure, but as restrictions on it. From a more practical perspective, this means that types tell you what you can do with an object, not what it is. So the type
{}and it's corresponding interface just tell you that there is no property you know how to interact with. Thus, it makes sense that these types would be something that can have properties (non-nullish), but not necessarily extendobject. The same goes forIt just describes the ability to read and store a
numberin thelengthproperty, not that the containing object is non-primitive. Actually needing an non-primitive object is rare and mostly comes down to calling internal library functions with that requirement.As such I think the ideal sollution is to keep all interfaces and object types possibly primitive and only add that requirement when
& objector the proposedextends objectis added. After all, being non-primitive is almost never (correctly) used in practice. Hans Brende (@HansBrende) already mentionedObject.createandObject.setPrototypeOfand I agree that I have barely seen other use cases for it.To cover the problem of obviously non-primitive objects not being assignable to
object, having (JS) object literals, classes and all things with call and construt signatures (you all seem to forget that functions also extendobject) automatically extendobjectimplicitly would not break anything and allow passing them to functions specifically demandingobject. In other cases, one should not expect an actual object anyways and use type guards liketypeof x === 'object'to be safe, which already does narrow toobject | null, the most common appearance of theobjecttype as far as I have seen.In the rare cases one actually demands an argument or property to be/stay strictly non-primitive, it is always possible to add the
& objectorextends objectexplicitly. As the rare exception, this should be the explicit case.To summarize, this would mean leaving almost everything the same except:
objectwhen checked as parameter, written onto a variables, set as property in another literal or used as expression in some other way (e.g.({ a(this: object) {} }).a()).object,never,anyand things containing call or construt signatures assignable toobject. All other types must intersectobjectto be used as such (this includes{}to come back to the motivating example of this issue).extends objectfor interfaces as you suggested to avoid the need for another type with& objectappended. Possibly add this to a lot of standard library interfaces for built-in types likeObjectorNumberand so on.