Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"object" can be made to accept primitives due to handling of "{}" #56205

Closed
tom-wild-moose opened this issue Oct 24, 2023 · 4 comments
Closed
Labels
Unactionable There isn't something we can do with this issue

Comments

@tom-wild-moose
Copy link

tom-wild-moose commented Oct 24, 2023

πŸ”Ž Search Terms

object unknown null
inconsistent behavior around unknown and object
object accepts primitive types through unknown

πŸ•— Version & Regression Information

  • This changed between versions 4.7 and 4.8

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.3.0-dev.20231024#code/MYewdgzgLgBAZiEMC8MAUAPGAuGIBGAVgKbBQCUKAfDBgNwCwAUKJLPgIYBOK6WuAOXACArgBsxHfGOIAeEWADWYEAHcwVSshoIQmcoxbhoMTgC9emXAFEMwMSIAmchcrVgANDDDixMAD4wCs5wAJZgxI6a1PCI+obMnFxoAMwGzLqp6UzMrCa6AEyWGLgEJGQB3r6VwcRhEY5aNPS5xuzcRahWMEJgohJSMvJKKuo0gT4SNWAh4ZFNsSAF8a1sptwpxbiuo2ALhSs5TEnLAORSwKfZB+f4l9lAA

πŸ’» Code

const foo = (x : object) => x;
const bar = (x : NonNullable<unknown>) => foo(x); // No error
const baz = (x: Exclude<unknown, null | undefined>) => foo(x); // Error

bar(3); // No error
foo(3); // Error

const foo2 = (x: object | null | undefined) => x;
const bar2 = (x: NonNullable<unknown> | null | undefined) => foo2(x); // No error
const bar3 = (x: unknown) => foo2(x); // Error

bar2('abc'); // No error
foo2('abc'); // Error

πŸ™ Actual behavior

Only some of the cases in the code sample error, but bar and bar2 don't, despite passing a primitive to foo which expects an object.

This is because object accepts unknown values once they're checked for null/undefined (i.e. it accepts {} / NonNullable<unknown>), and {} accepts primitives, so primitives can be indirectly accepted as object.

Relatedly, in the case of unknown, NonNullable<T> ends up behaving differently than Exclude<T, null | undefined>.

πŸ™‚ Expected behavior

All the cases in the code sample should error, including bar and bar2 when given primitive values.

Generally, object is the type of all non-primitive values, so passing primitives to it should result in an error. Since NotNullable<unknown> (i.e. {}) includes primitives, passing it as object should result in an error.

Further, NotNullable<Type> is defined as "excluding null and undefined from Type", so Exclude<unknown, null | undefined> should behave the same as NonNullable<unknown> (and conversely NonNullable<unknown> | null | undefined should behave the same as unknown).

Additional information about the issue

This inconsistent behavior was introduced in TS 4.8.

Note that while the examples above may seem kind of esoteric, this also appears in the much more common case where {} is implicitly inferred thanks to a null check, e.g.:

const foo = (x : object) => x;
function bar(x : unknown) {
  if (x != null) foo(x); // No error since v4.8
}

If I had to guess, I suppose it might be due to conflating {}'s actual meaning (which appears to be "any non-nullish value" - that is, including primitives) vs. what it visually looks like, which is an object.

@fatcerberus
Copy link

fatcerberus commented Oct 24, 2023

This isn't unique to {} fwiw - you can also assign primitives to e.g. { toString(): string } which is assignable to object for obvious reasons. This is a known unsoundness with object iirc, because object types accept anything that is coercible to that object.

@MartinJohns
Copy link
Contributor

MartinJohns commented Oct 24, 2023

Exclude<unknown, null | undefined> does absolutely nothing and resolves to unknown. Exclude<> is used to remove types from a union type, but unknown is not a union type. To represent a type "unknown, except null and undefined" you'd need Negated Types: #4196

@RyanCavanaugh RyanCavanaugh added the Unactionable There isn't something we can do with this issue label Oct 24, 2023
@RyanCavanaugh
Copy link
Member

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.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Unactionable" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Oct 27, 2023
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Unactionable There isn't something we can do with this issue
Projects
None yet
Development

No branches or pull requests

5 participants