
What is an object in JavaScript?
You’ll often hear that “everything is an object” in JavaScript — but that’s not quite true. Primitives like string, number, or boolean are not objects. The confusion likely comes from the fact that primitives behave like objects in many cases.
This is due to JavaScript’s auto-boxing mechanism:
Primitives have no methods but still behave as if they do. When properties are accessed on primitives, JavaScript auto-boxes the value into a wrapper object and accesses the property on that object instead. — MDN
Take strings, for example. A primitive string and a String object differ in two key ways.
First, their types are different:
console.log(typeof 'Hello') // 'string'
console.log(typeof new String('world!')) // 'object'
Second, you can’t add properties to a primitive:
const stringPrimitive = 'foo'
// attempt to add a property
stringPrimitive.foo = 'bar'
// the property is silently dropped
console.log(stringPrimitive.foo) // undefined
TypeScript approach
TypeScript needs to reflect JavaScript’s behaviour: a value’s shape (how it can be used) isn’t always tied to its type (what it is). TypeScript separates these two concerns.
“How”: Structural Typing Example
Consider this logger function:
function logLength(value: { length: number }) {
if(typeof value === 'string') {
console.log('String length is:', value.length);
return;
}
if (Array.isArray(value)) {
console.log('Array length is:', value.length);
return;
}
console.log('Length is:', value.length);
}
This function accepts any value that has a length: number property.
logLength('Hello world!') // String length is: 12
logLength(['Hello', 'world!']) // Array length is: 2
String primitives, arrays, and any object with a length: number property are valid. TypeScript uses structural typing here—the value just needs to have the right shape, regardless of what it actually is.
“How”: String Interface Example
Similarly, the String interface describes the shape of a string value—it covers both string primitives and String objects:
const stringPrimitive: String = "Hello";
const stringObject: String = new String("world!");
This tells TypeScript how the value can be used (methods like toUpperCase, charAt, etc.) but not what the value actually is.
“What”: string Type Example
By contrast, the string type describes what the value is: a primitive string. But because string extends the String interface, it also implicitly defines how the value can be used.
const stringPrimitive: string = "Hello";
const stringObject: string = new String("world!");
// ^^^^^^^^^^^^^
// Type 'String' is not assignable to type 'string'.
Only actual string primitives are assignable to string, even though String instances have the same interface.
Introducing the object type
In TypeScript, the object type is meant to represent any non-primitive value.
TypeScript did not have a type that represents the non-primitive type, i.e. any thing that is not number, string, boolean, symbol, null, or undefined — www.typescriptlang.org
The Expected: Rejecting Primitives
As expected, TypeScript rejects primitives where a non-primitive is required. For example, Object.create() expects an argument of type object:
const stringValue = 'definitelyPrimitive';
Object.create(stringValue);
// ^^^^^^^^^^^
// Argument of type 'string' is not assignable to
// parameter of type 'object'.
The Unexpected: Allowing Uncertainty
However, if the value’s type might be an object, TypeScript allows it — even if that leads to a runtime error.
const uncertainType: String = 'couldBeObject';
Object.create(uncertainType); // ✅ compiles, ❌ crashes at runtime
This is inconsistent. TypeScript usually forces you to narrow down uncertain types before using them. Consider:
let figure: string | number;
Math.round(figure);
// ^^^^^^
// Type 'string' is not assignable to type 'number'.
So on one hand, TypeScript prevents using a known primitive where an object is required. But on the other, it permits ambiguous types — even if they might be primitives — without narrowing. That breaks with TypeScript’s usual “narrow or error” principle.
The Flaw of the object Type
object and string
Let’s start with the part TypeScript gets right. When comparing object to string, we see expected behaviour:
- object does not extend string
- string does not extend object
- object & string resolves to never

This holds true for all primitive types: they don’t overlap with object, which makes sense.
object and { }
Things get weird when we compare object with {}.
The {} type represents all non-nullish values—everything except null and undefined. So it includes both primitives and non-primitives.
So far, this is expected:
- object extends {} (since all objects are non-nullish)
- object & {} resolves to object
But here’s the problem:
- {} also extends object
This is a contradiction. If object only includes non-primitives, and {} includes primitives, then allowing {} to extend object breaks the model.

Broken Transitivity
This leads to a failure in a core principle of TypeScript’s type system:
If A extends B and B extends C, then A extends C.
This should be true every time, right?
In reality:
- string does not extends object

Yet:
- String extends object
- string extends String

So transitivity breaks.

The Core Flaw
This inconsistency is rooted in an implicit assumption in the type system:
If it might not be a primitive, then it extends object.
This logic causes TypeScript to treat some uncertain or overlapping types (like String, or {}) as object, even when it results in unsound behaviour.
Why this flaw exists
When TypeScript behaves unexpectedly, it’s often because it optimizes for the 99% use case. This flaw likely exists to keep things simple for most developers.
But this issue only appears in edge cases, like:
- Deliberately using the String interface instead of the string primitive (which TypeScript explicitly discourages)
- Designing structural types (e.g. { length: number }) that can be satisfied by primitives
These scenarios are rare — but they’re valid.
Still, simplicity for the common case doesn’t justify inconsistency in the rare case. TypeScript could preserve predictable, sound behaviour without sacrificing usability for the majority.
Finding a fix
Introducing Implicitly Non-Primitive Types
As we’ve seen, the object type is intended to represent non-primitives. But we can infer non-primitiveness without using object explicitly.
Any structural type that no primitive satisfies — like { foo: 'bar' }—is, by definition, non-primitive. This can be inferred automatically:
function primitiveForbidden(arg: object) {}
primitiveForbidden({ foo: "bar" }); // ✅ non-primitive
In practice, the object keyword would only be needed when the type is uncertain or intentionally generic.
This Behavior Already Exists (Partially)
This isn’t hypothetical — TypeScript’s control flow analysis already does something similar:
let mightBeString: { length: number };
if(typeof mightBeString === 'string') {
mightBeString; // inferred as string
}
let definitelyNotString: { foo: 'bar' };
if(typeof definitelyNotString === 'string') {
definitelyNotString; // inferred as never
}
When TypeScript sees a shape that can’t possibly match a primitive, it narrows it out automatically. This is already a partial form of implicit non-primitive tagging.
Extending this inference more broadly could eliminate the need for object in most real-world use cases.
Do We Need Explicit Non-Primitives?
Would this shift require writing a lot of explicit & object annotations?
Probably not.
In most cases, this explicit tagging would be inferred on its own:
let stringish = { length: 10 }; // inferred as object & { length: number }
stringish = 'test'; // would now be invalid
if(typeof stringish === 'string') {
// would now be infered as never
}
For the remaining uncertain types, two approach are possible:
- Explicitly add & object to disambiguate
- Narrow types when needed
For typical codebases, this change would affect almost nothing.
The End of Branded Types
The real disruption is a side effect: branded types would break.
Right now, branded types often rely on intersecting a primitive with a unique shape:
type Id = string & { __brand: 'id' }
With implicit non-primitiveness, this would now resolve to never—because { __brand: … } would never match a primitive.
That would make code like this invalid:
const id: Id = 'abc' // Type '"abc"' is not assignable to type 'never'
This is significant. Branding is commonly used to distinguish structurally identical types — like IDs or properties from different entities. It prevents mistakes like this:
type ProductId = number;
const getProductById = (id: ProductId) => {
// Logic to retrieve product
}
type UserId = number;
const id: UserId = 1
getProductById(id) // No type error, but incorrect usage
Libraries like ts-brand or frameworks like Effect rely on this pattern. Implicit non-primitives would break these techniques unless an alternative branding mechanism were introduced.
Final thoughts
This is not a new issue — multiple GitHub threads have discussed it, and it continues to resurface.
The problem tends to go unnoticed because TypeScript behaves correctly in most cases. But once you start digging into why object and {} exist separately—and why they extend each other—it becomes clear that there's an inconsistency at the core of the type system.
Introducing a compiler option like strictNonPrimitiveCheck could resolve this without breaking common usage. It would bring clarity to how object is meant to behave and eliminate subtle, unsound edge cases.
Links to experiments:
À propos d’ekino
Le groupe ekino accompagne les grands groupes et les start-up dans leur transformation depuis plus de 10 ans, en les aidant à imaginer et réaliser leurs services numériques et en déployant de nouvelles méthodologies au sein des équipes projets. Pionnier dans son approche holistique, ekino s’appuie sur la synergie de ses expertises pour construire des solutions pérennes et cohérentes.
Pour en savoir plus, rendez-vous sur notre site — ekino.fr.
TypeScript “object” doesn’t make sense was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.
