Excess property checks
Learn why TypeScript rejects a typo in an object you write inline but not the same object from a variable, and how to allow extra properties when you mean to.
- 01Spot a fresh objectTell when an object is written right where a type is expected, so TypeScript checks it for extra properties.
- 02Read the two messagesRead
Object literal may only specify known propertiesand itsDid you meanform, and fix the typo it points at. - 03Know when it's skippedExplain why objects from variables, spreads, and
asget through, and choose a safe way to allow an extra property.
A typo caught in a new object
A settings page lets a shopper pick a theme and a text size. The code passes the choices to a function as one object. One property name has a typo: fontSise instead of fontSize. Here is what plain JavaScript does:
function applySettings(settings) {
const size = settings.fontSize ?? 14;
console.log(`${settings.theme} theme, ${size}px text`);
}
applySettings({ theme: "dark", fontSise: 18 });Line 1 starts a function that takes a settings object. Line 2 reads settings.fontSize, and the ?? (nullish coalescing) uses 14 when it's missing. Line 3 prints the theme and the size. Line 6 calls the function with fontSise: 18. The object has no fontSize, so it prints dark theme, 14px text. The shopper asked for 18 and got 14, and nothing warned anyone.
Now the same code in TypeScript, with a type for the settings:
type Settings = { theme: string; fontSize?: number }; function applySettings(settings: Settings) { const size = settings.fontSize ?? 14; console.log(`${settings.theme} theme, ${size}px text`);} applySettings({ theme: "dark", fontSise: 18 });Line 1 makes a type called Settings: a theme that is text, and a fontSize that is a number. The ? after fontSize means it can be left out (Object types covers this). Line 3 says applySettings takes a Settings. Line 8 is the call, and TypeScript marks it, on purpose here, because it's the bug:
Object literal may only specify known properties, but 'fontSise' does not exist in type 'Settings'means "this new object has a property thatSettingsdoesn't list". An object literal is an object you write out with curly braces, like{ theme: "dark" }.Did you mean to write 'fontSize'?is TypeScript's guess at the fix: the listed name closest to the one you wrote.
The code still runs and prints dark theme, 14px text; the error is a warning for you. Fix the name on line 8, and it prints dark theme, 18px text.
Settings. theme is listed. fontSise isn't, so line 8 gets an error.This is an excess property check. "Excess" means extra: more than the type asks for. In the Structural typing lesson, extra properties were fine. An object only needed the properties its type lists. That's still true for most objects. But an object you write right where a type is expected gets this one extra check.
At a post office counter, you fill in a delivery form while the clerk watches. If you write a line the form doesn't have, the clerk asks about it straight away: it's probably a slip of the pen. But when you bring a parcel you packed at home, the clerk only checks that the label has an address. What else is inside is your business.
- In real life: You fill in a delivery form at the counter
- In TypeScript: You write an object right where a type is expected
- In real life: The clerk reads every line you just wrote
- In TypeScript: TypeScript compares every property with the type
- In real life: "Postcdoe? Did you mean Postcode?"
- In TypeScript:
Did you mean to write 'fontSize'? - In real life: A parcel packed at home: the clerk only checks the label
- In TypeScript: An object from a variable: TypeScript checks only what the type needs
An object literal written right where a type is expected may only have the properties that type lists. Any other property is an error. Nothing else in your code can ever read that object's extra properties, so TypeScript treats an extra name as a mistake, usually a typo.
Fresh object literals
An object literal is called fresh while it's still the object you just wrote, before it's stored anywhere. A fresh object meets a type in four common places. Each one here has the extra property qty on purpose:
type CartItem = { name: string; price: number }; const tea: CartItem = { name: "Tea", price: 3, qty: 2 }; function addToCart(item: CartItem) { return item.name;}addToCart({ name: "Cake", price: 4, qty: 1 }); function freeGift(): CartItem { return { name: "Mint", price: 0, qty: 1 };} const cart: CartItem[] = [{ name: "Jam", price: 5, qty: 3 }];Line 1 makes a CartItem type with a name and a price. Then:
- Line 3 writes an object into a variable with a type,
const tea: CartItem. - Line 8 writes an object as an argument to
addToCart, which takes aCartItem. - Line 11 returns an object from
freeGift, which says it gives back aCartItem. - Line 14 writes an object inside an array of
CartItems.
All four get the same error: 'qty' does not exist in type 'CartItem'. Delete qty from any line with Edit & run, and that line's error disappears.
CartItem is expected, its extra qty gets an error: lines 3, 8, 11, and 14.The check also reaches inside. An object written inside another fresh object is fresh too:
type Order = { id: number; customer: { name: string; email: string } }; const order: Order = { id: 7, customer: { name: "Asha", emial: "asha@example.com" },};Line 1 says an Order has an id and a customer with a name and an email. Line 3 starts an order. Line 5 writes the customer with emial. TypeScript reports line 5 and asks Did you mean to write 'email'? The order on line 3 isn't marked, because its own two properties are right.
Catching typos
The check helps most with names that can be left out. A missing optional property is allowed, so without this check a misspelled one would be silently ignored. Type a name for the second property on line 3, and watch the list under the code:
type Settings = { theme: string; fontSize?: number }; const settings: Settings = { theme: "dark", fontSise: 18 };console.log(settings.fontSize ?? 14);fontSise1 error: line 314fontSise is close to fontSize, so TypeScript reports it and suggests the fix: Object literal may only specify known properties, but 'fontSise' does not exist in type 'Settings'. Did you mean to write 'fontSize'? Run it anyway, and the 18 is lost: it prints 14.
fontSize is the only name that has no error, and then line 4 prints 18. A name close to a real one, like fontsize, gets a Did you mean suggestion. A name that isn't close to anything, like zoom or size, gets the shorter message with no suggestion. With every wrong name, line 4 prints 14, because the 18 sits under a name nothing reads.
The table below shows the two messages. Read each row as: the kind of extra name, what TypeScript says, and its error number.
| When the extra name is… | What TypeScript says | Error number |
|---|---|---|
The extra name is close to a real one, like fontSise | Object literal may only specify known properties, but 'fontSise' does not exist in type 'Settings'. Did you mean to write 'fontSize'? | 2561 |
The extra name isn't close to any real one, like zoom | Object literal may only specify known properties, and 'zoom' does not exist in type 'Settings'. | 2353 |
What if the misspelled property is one the type needs? Here fontSize has no ?:
type Settings = { theme: string; fontSize: number }; const settings: Settings = { theme: "dark", fontSise: 18 };Line 1 says every Settings needs a fontSize. Line 3 has fontSise instead. You might expect Property 'fontSize' is missing. TypeScript reports the extra name instead, with the same Did you mean to write 'fontSize'?, which points straight at the fix. Once you fix the name, both problems are gone.
When the check doesn't apply
The check only runs on a fresh object. Once the object is stored somewhere first, it's an ordinary value, and the usual rule from Structural typing applies: it must have what the type needs, and extra properties are fine. Press each button to get the same settings to applySettings a different way:
type Settings = { theme: string; fontSize?: number }; function applySettings(settings: Settings) { const size = settings.fontSize ?? 14; console.log(`${settings.theme} theme, ${size}px text`);} applySettings({ theme: "dark", fontSise: 18 });dark theme, 14px textYes1 error: line 8The object is written right in the call, so it's fresh. TypeScript checks it for extra properties and reports fontSise on line 8.
Written in the call and A variable with a type both write the object right where Settings is expected, so both get the error. From a variable stores it in saved with no type first: no error, and the code prints dark theme, 14px text. With as Settings uses a type assertion: as Settings after a value tells TypeScript "treat this as a Settings, I know what I'm doing", and the check is skipped. Only Typo fixed prints dark theme, 18px text.
Settings, so an extra name is a mistake. Right: once it's stored in saved, other code could still read saved.fontSise, so TypeScript only checks what Settings needs.Why the difference? A fresh object written in the call can only ever be read as a Settings, so nothing could use fontSise. A stored object is different: other code could read saved.fontSise, so its extra properties might be on purpose. TypeScript only flags extras where they can't be anything but a mistake.
Properties copied in with a spread, ..., aren't checked either:
type Settings = { theme: string; fontSize?: number }; const saved = { fontSise: 18 };// ^?const settings: Settings = { theme: "dark", ...saved };console.log(settings.fontSize ?? 14);Line 3 stores { fontSise: 18 } in saved. The query on line 4 shows its type, { fontSise: number; }, typo and all. Line 5 writes a fresh object with a theme and copies in everything from saved. TypeScript only checks the properties written by hand, so there's no error, and line 6 prints 14.
One more check works on variables too. When every property of a type is optional, TypeScript calls it a weak type, and an object must share at least one property with it:
type ButtonOptions = { color?: string; size?: number }; function buttonClass(options: ButtonOptions) { return `btn btn-${options.color ?? "grey"}`;} const options = { colour: "red" };console.log(buttonClass(options));Line 1 makes ButtonOptions, where both properties have a ?. Line 7 stores { colour: "red" }, spelled the British way. Line 8 passes it in. options comes from a variable, but TypeScript still reports Type '{ colour: string; }' has no properties in common with type 'ButtonOptions': an object with none of the listed names can't be what you meant. Run it anyway and it prints btn btn-grey. If the object also had a size, it would share one property, and the colour typo would get through.
The table below sums up the section. Read each row as: how the object gets to the type, whether it's checked for extras, and why.
| How the object gets there | Checked for extras? | Why |
|---|---|---|
Written right in the call: applySettings({ … }) | Yes. | Nothing else can ever read the object, so an extra property is almost surely a mistake. |
Written in a variable with a type: const saved: Settings = { … } | Yes. | The object meets the type Settings the moment it's written. |
Stored in a variable first, then passed: applySettings(saved) | No. | Other code can still read saved, so its extra properties may be useful. The usual shape check runs. |
Copied in with a spread: { theme: "dark", ...saved } | No. | Only the properties you write by hand are checked. The ones a spread copies are not. |
Marked with as Settings | No. | as tells TypeScript to treat the object as a Settings, so it skips the check. |
Allowing extra properties on purpose
Sometimes the extra property is not a typo. A shop sends analytics events (small records of what shoppers do) to a tracking service, and one event needs a product ID the type doesn't list:
type TrackedEvent = { name: string }; function track(event: TrackedEvent) { console.log(JSON.stringify(event));} track({ name: "add_to_cart", productId: "p-7" });Line 1 says a TrackedEvent has a name. Line 3 starts track, and line 4 prints the event as text with JSON.stringify (JSON covers it). Line 7 sends productId, and TypeScript reports 'productId' does not exist in type 'TrackedEvent'. This time the property is wanted. The best fix is to say so in the type:
type TrackedEvent = { name: string; productId?: string }; function track(event: TrackedEvent) { console.log(JSON.stringify(event));} track({ name: "add_to_cart", productId: "p-7" });track({ name: "add_to_cart", prodcutId: "p-8" });Line 1 adds productId?: string. Line 7 now has no error, and line 4 prints {"name":"add_to_cart","productId":"p-7"}. The check still protects you: line 8 misspells it as prodcutId, and TypeScript asks Did you mean to write 'productId'?
When the extra names really can't be known ahead of time, the type can say "any other name is fine, as long as its value is text". That's an index signature, and the next lesson, Index signatures & Record, is about them:
type TrackedEvent = { name: string; [detail: string]: string }; function track(event: TrackedEvent) { console.log(JSON.stringify(event));} track({ name: "add_to_cart", productId: "p-7" });track({ name: "search", query: "green tea" });Line 1 adds [detail: string]: string: any property name, with a string value. Lines 7 and 8 send different details, productId and query, and neither is an error. The price of this freedom: a typo like prodcutId would be allowed too.
The table below compares the ways to make the error go away. Read each row as: what you do, what TypeScript still checks, and when to use it.
| What you do | What TypeScript still checks | When to use it |
|---|---|---|
Add the property to the type: productId?: string | Every other property, and the new one's type. A typo like prodcutId still gets an error. | Best when you know the name. Do this first. |
Add an index signature: [detail: string]: string | Only that every extra value is a string. Any name is allowed, typos too. | When the names really can't be known ahead of time. |
| Store the object in a variable first | Only the properties the type needs. | Rarely. It hides the check without saying why. |
Write as TrackedEvent after the object | Almost nothing about the extras. | Avoid it for this. It switches the check off and hides typos. |
Options for a product search
Option objects are where this check helps most. A search box builds a link from a search word and a few optional settings. Every setting can be left out, so a misspelled one would quietly do nothing:
type SearchOptions = { query: string; sortBy?: string; limit?: number }; function searchUrl(options: SearchOptions) { const limit = options.limit ?? 20; return `/search?q=${options.query}&limit=${limit}`;} console.log(searchUrl({ query: "tea", limt: 5 }));Line 1 says a search has a query, and may have a sortBy and a limit. Line 4 uses 20 results when no limit is given. Line 5 builds the link. Line 8 asks for 5 results but writes limt. TypeScript asks Did you mean to write 'limit'? Run it anyway and it prints /search?q=tea&limit=20: the shopper gets 20 results instead of 5.
type SearchOptions = { query: string; sortBy?: string; limit?: number }; function searchUrl(options: SearchOptions) { const limit = options.limit ?? 20; return `/search?q=${options.query}&limit=${limit}`;} console.log(searchUrl({ query: "tea", limit: 5 }));Only line 8 changed: limit: 5. The error is gone, and it prints /search?q=tea&limit=5.
const s: { theme: string } = { theme: "dark", color: "red" };const saved = { theme: "dark", color: "red" }; const s: { theme: string } = saved;const s: { fontSize?: number } = { fontSise: 16 };const s = { theme: "dark", color: "red" } as { theme: string };const opts = { colour: "red" }; const o: { color?: string } = opts;const opts = { colour: "red", size: 2 }; const o: { color?: string; size?: number } = opts;const list: { name: string }[] = [{ name: "Tea", price: 3 }];const extra = { price: 3 }; const item: { name: string } = { name: "Tea", ...extra };
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
- "Objects can never have extra properties in TypeScript." No. Only a fresh object literal is checked for extras. An object from a variable can have as many extras as it likes, as long as it has what the type needs.
- "Storing the object in a variable first is the way to allow an extra property." No. It hides the check without saying why. Add the property to the type, so typos in it are still caught.
- "
as Settingschecks the object more strictly." No. A type assertion tells TypeScript to trust you, and it skips the excess property check. - "TypeScript lists every extra property at once." No. It reports the first extra property in an object. Fix that one, and it shows the next.
- "The error removes the extra property." No. The types are gone when the code runs, and the object keeps every property. That's why the overview still prints
14px: the 18 is there, under the wrong name. - "Spread objects are checked too." No. Only properties you write by hand in the literal are checked. Ones copied in with
...get through.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
A user profile with a typo. Which name should line 3 use?
type Profile = { name: string; avatarUrl?: string };
const profile: Profile = { name: "Asha", avatarURL: "/avatars/asha.png" };
console.log(profile.avatarUrl ?? "/avatars/default.png");type Profile = { name: string; avatarUrl?: string }; const profile: Profile = { name: "Asha", avatarUrl: "/avatars/asha.png" };console.log(profile.avatarUrl ?? "/avatars/default.png");Profile lists avatarUrl, and line 3 writes avatarURL. TypeScript asks Did you mean to write 'avatarUrl'?. With the right name, the code prints /avatars/asha.png instead of the default picture.
Two addresses, both with a floor. Which line does TypeScript mark?
type Address = { street: string; city: string };
function shipTo(address: Address) {
return `${address.street}, ${address.city}`;
}
const home = { street: "12 Park Road", city: "Pune", floor: 2 };
console.log(shipTo(home));
console.log(shipTo({ street: "4 Lake View", city: "Goa", floor: 1 }));type Address = { street: string; city: string }; function shipTo(address: Address) { return `${address.street}, ${address.city}`;} const home = { street: "12 Park Road", city: "Pune", floor: 2 };console.log(shipTo(home)); // fine: home is a variableconsole.log(shipTo({ street: "4 Lake View", city: "Goa", floor: 1 })); // line 9: floor is extraLine 9 writes the object right in the call, so its extra floor is checked and reported. Line 8 passes home, a variable, which has the street and city that Address needs, so its floor is allowed.
A toast is a small message that pops up for a few seconds. Fix line 8. What does the fixed code print?
type ToastOptions = { message: string; durationMs?: number }; function toastText(options: ToastOptions) { const seconds = (options.durationMs ?? 3000) / 1000; return `${options.message} (${seconds}s)`;} console.log(toastText({ message: "Saved", duration: 5000 }));type ToastOptions = { message: string; durationMs?: number }; function toastText(options: ToastOptions) { const seconds = (options.durationMs ?? 3000) / 1000; return `${options.message} (${seconds}s)`;} console.log(toastText({ message: "Saved", durationMs: 5000 }));duration isn't in ToastOptions; TypeScript suggests durationMs. With the right name, 5000 milliseconds is 5 seconds, so it prints Saved (5s) instead of Saved (3s).
The event really needs its productId. Make line 7 pass without as, and print the name followed by the ID. What does your code print?
type TrackedEvent = { name: string }; function describe(event: TrackedEvent) { return event.name;} console.log(describe({ name: "add_to_cart", productId: "p-7" }));type TrackedEvent = { name: string; productId?: string }; function describe(event: TrackedEvent) { return `${event.name} ${event.productId ?? ""}`.trim();} console.log(describe({ name: "add_to_cart", productId: "p-7" }));Adding productId?: string to TrackedEvent says events may carry it, so line 7 has no error and typos in the name are still caught. describe adds the ID after the name and prints add_to_cart p-7.
TypeScript reports nothing here. What does it print? Then change line 3 so a typo there would get an error, and fix the typo.
type Settings = { theme: string; fontSize?: number }; const fromStorage = { fontSise: 18 };const settings: Settings = { theme: "dark", ...fromStorage };console.log(settings.fontSize ?? 14);type Settings = { theme: string; fontSize?: number }; const fromStorage: { fontSize?: number } = { fontSize: 18 };const settings: Settings = { theme: "dark", ...fromStorage };console.log(settings.fontSize ?? 14);The starter prints 14: fontSise arrives through the spread, which isn't checked. Writing const fromStorage: { fontSize?: number } = { … } makes that object fresh where it meets a type, so fontSise would be an error there. With the name fixed, it prints 18.
Why does TypeScript mark line 8 even though options is a variable? What does the code print if you run it anyway? Then fix it.
type ButtonOptions = { color?: string; size?: number }; function buttonClass(options: ButtonOptions) { return `btn btn-${options.color ?? "grey"}`;} const options = { colour: "red" };console.log(buttonClass(options));type ButtonOptions = { color?: string; size?: number }; function buttonClass(options: ButtonOptions) { return `btn btn-${options.color ?? "grey"}`;} const options = { color: "red" };console.log(buttonClass(options));colour isn't color, so the object shares no property with ButtonOptions, and TypeScript reports it even from a variable. If you run it anyway, options.color is missing and it prints btn btn-grey. Spelled color, it prints btn btn-red.
Check your understanding
For each question, ask where the object was written: right where the type is expected, or somewhere else first. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7Which objects does TypeScript check for extra properties?
Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in this coupon code?
Read the code, then predicttype Coupon = { code: string; percent: number }; const saved = { code: "SAVE10", percent: 10, expires: "2025-12-31" }; const a: Coupon = saved; const b: Coupon = { code: "TEA5", percent: 5, expires: "2025-06-30" };Choose an answer to see the explanation.
Question 3 of 7What does this settings code print, and does TypeScript complain?
Read the code, then predicttype Settings = { theme: string; fontSize?: number }; const settings = { theme: "light", fontSise: 20 } as Settings; console.log(settings.fontSize ?? 14);Choose an answer to see the explanation.
Question 4 of 7What does this profile code print when the city comes in through a spread?
Read the code, then predicttype Profile = { name: string; city?: string }; const fromForm = { name: "Asha", citty: "Pune" }; const profile: Profile = { ...fromForm }; console.log(profile.city ?? "unknown");Choose an answer to see the explanation.
Question 5 of 7Where does TypeScript report an error in this filters code?
Read the code, then predicttype Filters = { inStock?: boolean; maxPrice?: number }; function countFilters(filters: Filters) { return Object.keys(filters).length; } const picked = { instock: true }; console.log(countFilters(picked));Choose an answer to see the explanation.
Question 6 of 7How many errors does TypeScript report for line 3 of this settings object?
Read the code, then predicttype Settings = { theme: string }; const settings: Settings = { theme: "dark", color: "red", zoom: 2 };Choose an answer to see the explanation.
Question 7 of 7An analytics event really needs a
productId. What's the safest fix for the extra-property error?Choose an answer to see the explanation.
Key takeaways
- An object literal written right where a type is expected is fresh, and may only have the properties that type lists.
- The check runs for typed variables, arguments, return values, array items, and objects nested inside them.
- A name close to a listed one gets
Did you mean to write …?(2561); any other extra name gets 2353. - Objects from variables, properties from a spread, and objects marked with
asskip the check. - A type whose properties are all optional also needs at least one shared property, even from a variable.
- To allow an extra property, add it to the type. Use an index signature only when the names can't be known.
Remember the one-liner.
A new object written where a type is expected may only say what the type lists, so a typo there is caught.
Coming next: Index signatures & Record. It types objects whose property names you don't know ahead of time, like lookup tables and dictionaries.