My TypeScript Adventures: Excess Props, `object` vs `{}`, and Why My Code Sometimes Explodess
Tripped over excess property checks in TypeScript — sharing what I learned so you don’t.
Hey friends — I’ve been fiddling with TypeScript lately, trying to understand all those little gotchas. One thing that kept biting me was excess property checks, plus the difference between object
, {}
, Object
types. I want to share what I learned, because earlier I was confused, maybe you will too, and maybe this helps you avoid stepping on the same banana peel 🍌.
What even are excess property checks?
So imagine I define:
type Person = { firstName: string; age: number };
function logPerson(person: Person) {
console.log(person);
}
Then I try
const person2: Person = { firstName: "potato", age: 21, extraProp: "hello!" }; // <-- error
At first I was like “wtf, I thought TypeScript was structural, isn’t extraProp ok if it has all the required ones?” But no — TS is stricter when you assign an object literal directly to a typed variable (or pass literal directly to a function). It checks: “does this literal have any properties I don’t know about (i.e. not in the target type)?” If yes → error. That’s the “excess property check.” (TypeScript)
But then, I did this:
const person3: Person = {
firstName: "potato",
age: 21,
...{ extraProp: "hello!" },
}; // fine
AND it passed. And I was like “omg why?” 🤔 Turns out: because of how TS handles spreads and variables/inference. If you spread in extra props, or assign literal to a variable first, TS doesn’t always trigger that excess prop check. The literal isn’t “fresh” in a sense, so TS is more forgiving. (allthingstypescript.dev)
Why this behavior exists (and my struggle)
- It’s super helpful for catching little mistakes: typos, misspelling property names, etc. For example, you meant
color
but typedcolour
, TS might warn. (TypeScript) - But the inconsistency annoyed me. I kept wondering: “why does doing
.push(...)
in one case break, but in another, no?” Or “why does spreading let me sneak extra props in?” - Eventually I saw: TS is structural (duck typing), but it gives special treatment to object literals when creating or assigning them. If something has already been inferred (object assigned to a variable first), then “excess property” checks are often skipped. (allthingstypescript.dev)
So yeah, it feels inconsistent if you don’t know the rules. But once you know them, you can predict what will happen. (Still sometimes surprises me, don’t lie.)
object
vs {}
vs Object
— my head almost exploded
While learning excess props, I also got confused about these three. They seem like they’d be the same, but noooo. TS is picky in its own special way. Here’s how I think of them (after many StackOverflow lurks + making tiny example code to test) :
let foo: object; | let foo : {}; |
---|---|
foo = { hello: 0}; ✅ foo = []; ✅ foo = false;❌ foo = null;❌ foo = undefined; ❌ foo = 42;❌ foo = ‘bar’;❌ | foo = { hello: 0}; ✅ foo = [];✅ foo = false;✅ foo = null;❌ foo = undefined;❌ foo = 42; ✅ foo = ‘bar’✅ |
Type | What it allows | What it forbids | When I might use it |
---|---|---|---|
object (lowercase) | non-primitive things: objects, arrays, functions etc. (DEV Community) | primitives like number, string, boolean, symbol etc. (DEV Community) | When I really want “this must be an object (or array etc.), not just a string or number” |
{} (empty object type) | almost everything except null or undefined — yes you can pass string, number, bool etc. (Type-Level TypeScript) | basically only null and undefined are excluded (if strictNullChecks on) (Type-Level TypeScript) | When I don’t care much about structure, or I want super loose type (but that’s dangerous) |
Object (capital O) | similar to {} , lots of overlap; but has some weirdness, prototypical methods etc. (jser.dev) | maybe stricter in some built-in method typings; also semantically confusing (some people avoid using it) | I try to avoid; if I use it, it’s for “just anything with the base Object” but clarity suffers |
So: if I want more type safety, object
is usually safer than {}
in my code. {}
is too broad; you can accidentally pass a "hello"
or 123
and TS won’t complain. (Yes, I tested this.) (Type-Level TypeScript)
That “array union” snippet & mutation weirdness
You also gave the snippet:
const foo: string[] = ["1", "2"];
function bar(things: (number | string)[]) {
things.push(3); // passes type check
}
bar(foo);
I remember doing similar stupid stuff. What’s going on:
foo
isstring[]
bar
wants(number|string)[]
- Because every
string
is acceptable where(number|string)
is expected,string[]
is assignable to(string|number)[]
. (Covariance in this context) - So TS lets you call
bar(foo)
. Then insidebar
, pushing3
(a number) is allowed because the type ofthings
is(string|number)[]
. So at runtime, you end up with a mix. Type safety is “ok” by TS’s rules, but logically yourfoo
array has now numbers in it (which you might not expect). Scary.
So TS gives you power, but also begs you to pay attention. If I want safer, I might use readonly string[]
or avoid that mutation. (One of my dev regrets: I didn’t use readonly arrays earlier.)
My “aha!” moments & little tips
Here are some bits I picked up that really helped me stop banging my head:
- If you want to catch extra props always, try to annotate variables explicitly rather than letting TS infer, especially with literals. The moment you introduce a variable, sometimes the excess check is skipped.
- Use spreads carefully. Spreads sometimes “hide” excess props, which might be what you want, but sometimes what you don’t want, so know when it happens.
- Consider tools / lint rules. There are TS/ESLint rules that enforce stricter property checks, type assertions policies etc. These help keep things consistent.
- Be conservative with
object
,{}
etc. If you know your data shape, define interface/type. Don’t default to “anything goes.” Saves future debugging.
Final thoughts (because I’m still learning!)
Honestly, understanding these quirks felt like unlocking secret levels in TS. Sometimes I feel TS is beautiful, other times it’s like one more trap waiting. But the more I write, experiment, make mistakes (omg so many), the more these behavior patterns stick.
If you’re a beginner: don’t let it discourage you. It’s OK to get weird errors. Try small isolated examples like above. Try modifying them and see what TS complains about. Throw in a spread, move to variable first, assign literal, pass directly to function — see where TS flips. That kind of play is what taught me.
If you want, I can write up a mini-cheat sheet you can keep open, with all these “when does excess prop check happen / when not / object vs {} etc.” Want me to drop that?