-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
π 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
π» 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.