Property flags & descriptors
Learn how JavaScript property descriptors control writable, enumerable, and configurable behavior, and how definition differs from assignment.
- 01Read the hidden settings panelUse getOwnPropertyDescriptor and getOwnPropertyDescriptors to inspect real property flags.
- 02Install properties deliberatelyUse Object.defineProperty, Object.defineProperties, and Object.create descriptor maps.
- 03Predict blocked operationsTell when assignment, deletion, enumeration, and redefinition succeed or fail.
Every property has switches
You already use object properties constantly: user.name, settings.theme, cart.total. Until now, a property may have looked like just a name and a value. JavaScript keeps a little more information than that. A property descriptor is the complete settings panel for one property.
This lesson is about data descriptors, the kind that hold a value. They have the value plus three flags: writable, enumerable, and configurable. The next lesson, Getters & setters, swaps the value box for get and set functions. That is called an accessor descriptor.
Imagine every property has a tiny settings panel behind the wall. Turning writable off makes the value read-only. Turning enumerable off makes it hidden from normal lists. Turning configurable off locks the panel itself.
- In real life: A light switch marked writable
- In JavaScript: Can assignment change the value?
- In real life: A display switch marked enumerable
- In JavaScript: Does the property show up in listings?
- In real life: A maintenance switch marked configurable
- In JavaScript: Can the switches be changed, or the door removed?
Where the analogy stops: The panel is not a real object you can edit directly. JavaScript exposes a copy through descriptor methods, and you ask the engine to install changes.
| Flag | When true | When false |
|---|---|---|
writable | Assignment can change the value of a data property. | Assignment cannot change the value. In strict code it throws TypeError; in sloppy code it silently leaves the value alone. |
enumerable | The property appears in Object.keys, for...in, object spread, and JSON.stringify. | The property is still readable by name, but normal listings skip it. |
configurable | The property can be deleted and most descriptor switches can be changed. | The property cannot be deleted or reconfigured. The one allowed tightening is writable: true to false. |
These flags explain why Math.PI cannot be overwritten, why some properties are invisible to Object.keys, and why libraries can expose a stable public API while keeping internal slots out of loops and JSON output.
Object.defineProperty installs the settings panel
INTERACTIVEOrdinary assignment goes through a property’s normal door: obj.x = 1. Definition is stronger. Object.defineProperty installs or rewrites the property rules directly. It takes the object, the property name, and a descriptor object.
"use strict";
const account = {};
Object.defineProperty(account, "id", {
value: 42,
writable: false,
enumerable: true,
configurable: false,
});
console.log(account.id); // 42The code says: install an id property with value 42, make it visible in listings, but do not allow assignment to replace it or a later descriptor to loosen the rules. Because CompleteFrontend’s site code is strict, a failed write throws a TypeError. In sloppy scripts, the same failed write silently leaves the old value.
"use strict";const panel = {};Object.defineProperty(panel, "choice", { value: "open", writable: true, enumerable: true, configurable: true,}); panel.choice = "changed";delete panel.choice;Object.keys(panel);JSON.stringify(panel);- Before
{"value":"open","writable":true,"enumerable":true,"configurable":true}assignchangeddeletetruekeys(none)json{}redefineokAfter{"writable":false,"enumerable":false,"configurable":false}
Fresh object each time. With writable=true, enumerable=true, configurable=true: assign: changed
Try all three switches. Notice the surprise: enumerable does not control whether you can read panel.choice. It controls whether listing tools see it. configurable is the lock that affects deletion and most redefinition.
getOwnPropertyDescriptor(s): inspect the real panel
STEP THROUGHJavaScript gives you two inspection tools. Use Object.getOwnPropertyDescriptor(obj, name) for one own property. Use Object.getOwnPropertyDescriptors(obj) for every own property at once. They do not walk the prototype chain.
Step through two ways to create a property, then compare the real descriptors JavaScript reports.
script
"use strict";Object.defineProperty(normal, "quiet", { value: 2 }); console.log(Object.getOwnPropertyDescriptor(normal, "visible"));console.log(Object.getOwnPropertyDescriptor(normal, "quiet"));A built-in property is a useful reality check. In current JavaScript, Math.PI is a data property whose descriptor says writable: false, enumerable: false, and configurable: false. That is why strict code cannot replace it, Object.keys(Math) does not show it, and you cannot delete it.
const pi = Object.getOwnPropertyDescriptor(Math, "PI");
console.log(pi.value);
console.log(pi.writable);
console.log(pi.configurable);getOwnPropertyDescriptor answers only for properties stored directly on that object. If a value comes from a prototype, inspect the prototype object or use your prototype knowledge from Object.create to climb the chain deliberately.
Default flags: assignment is friendly, definition is strict
The most important gotcha is that different creation paths choose different defaults. Object literal syntax and assignment create normal everyday properties: writable, enumerable, and configurable are all true. Object.defineProperty is the opposite: every flag you omit defaults to false.
| How the property is created | Default flags |
|---|---|
Object literal: { x: 1 } | writable: true, enumerable: true, configurable: true |
Assignment: obj.x = 1 | Creates a normal own property with all three flags true, if assignment is allowed. |
Object.defineProperty(obj, "x", { value: 1 }) | Every omitted flag is false: read-only, hidden, and non-configurable. |
This is deliberate. Definition is a low-level tool. If JavaScript guessed permissive flags while you were defining a property, it would be easy to accidentally expose or loosen an internal detail.
const sloppy = {};
Object.defineProperty(sloppy, "x", { value: 1, writable: false });
sloppy.x = 2;
console.log(sloppy.x);
(function strictMode() {
"use strict";
const strict = {};
Object.defineProperty(strict, "x", { value: 1, writable: false });
try { strict.x = 2; } catch (error) { console.log(error.name); }
})();The first half is a sloppy script: assigning to a non-writable property does not throw, and the value remains 1. The second half is strict, so the same write is reported as TypeError. The course uses strict modules, so the strict behavior is the one you should expect in lesson code.
Defining many properties
STEP THROUGHA descriptor map is an object whose keys are property names and whose values are descriptor objects. Two APIs use that shape: Object.defineProperties adds many descriptors to an existing object, and Object.create can create an object with descriptors in one step.
Follow a descriptor map through Object.create, Object.defineProperties, Object.keys, and full inspection.
script
"use strict"; id: { value: 42, enumerable: true }, role: { value: "admin", enumerable: true },}); Object.defineProperties(user, { token: { value: "abc", writable: false }, label: { value: "Ada", writable: true, enumerable: true },}); console.log(Object.keys(user));console.log(Object.getOwnPropertyDescriptors(user));A common use is creating visible public fields and hidden metadata together. In the walkthrough, token exists and can be read directly, but because enumerable was omitted it is skipped by Object.keys and JSON.stringify.
"use strict";
const user = Object.create(null, {
id: { value: 42, enumerable: true },
role: { value: "admin", enumerable: true },
});
Object.defineProperties(user, {
token: { value: "abc", writable: false },
label: { value: "Ada", writable: true, enumerable: true },
});
console.log(Object.keys(user));
console.log(Object.getOwnPropertyDescriptors(user));Assignment vs definition
STEP THROUGHAssignment and definition can look like two ways to “put a property on an object,” but they obey different rules. Assignment uses the normal property access machinery. It can call inherited setters and it respects inherited non-writable data properties. Definition writes an own descriptor directly onto the target object.
Assignment follows the first instruction JavaScript finds. If the prototype has a setter, that setter runs. defineProperty instead adds an own labeled property directly to the target object.
- In real life: Writing on the page normally
- In JavaScript: Assignment:
child.name = "Ada" - In real life: Following a note on an earlier page
- In JavaScript: An inherited setter that runs during assignment
- In real life: Adding a new labeled page
- In JavaScript: Definition:
Object.defineProperty(child, "name", ...)
Where the analogy stops: The notebook image is only about how a property is created. JavaScript still enforces descriptor rules.
Compare normal assignment with direct property definition.
script
"use strict";const log = []; set name(value) { log.push("setter:" + value); }};Object.defineProperty(proto, "locked", { value: 1, writable: false }); const child = Object.create(proto);child.name = "Ada";Object.defineProperty(child, "name", { value: "Ada", enumerable: true }); try { child.locked = 2; } catch (error) { log.push(error.name); }Object.defineProperty(child, "locked", { value: 2, enumerable: true }); console.log(log);console.log(child.name, child.locked);"use strict";
const log = [];
const proto = {
set name(value) { log.push("setter:" + value); }
};
Object.defineProperty(proto, "locked", { value: 1, writable: false });
const child = Object.create(proto);
child.name = "Ada";
Object.defineProperty(child, "name", { value: "Ada", enumerable: true });
try { child.locked = 2; } catch (error) { log.push(error.name); }
Object.defineProperty(child, "locked", { value: 2, enumerable: true });
console.log(log);
console.log(child.name, child.locked);This distinction matters when you work with objects created from prototypes, class instances, or library objects. If you want to trigger the public API, assign. If you need to install a precise descriptor, define.
Where you’ll use this
Descriptors are not something you use for every property. They are a professional tool for edges where “normal property” is too broad.
- Read-only public constants. A library can expose version information or a token name without letting consumers overwrite it by accident.
- Hidden metadata. Frameworks often store internal markers that should not appear in
Object.keys, object spread, or JSON output. - Compatibility wrappers. You can install a property now with a shape that a later getter or setter can replace when the next lesson’s accessor descriptors become useful.
- Testing and debugging. Inspecting descriptors helps explain why a write, loop, or delete behaves differently from a plain object literal.
- Assign to a property with
writable: true - Assign to an own data property with
writable: falsein strict code - Read
obj.secretwhensecrethasenumerable: false - See a non-enumerable property in
Object.keys(obj) - Delete a property with
configurable: true - Change
configurable: falseback totrue - Change
writable: truetofalseon a non-configurable property - Use
definePropertyto create an own property where a prototype has a setter
Sort each operation by what the descriptor rules allow. Assume strict code when assignment fails.
Misconceptions and one-way locks
“Non-enumerable means private.”
No. It is hidden from common listings, but anyone who knows the name can read it, and getOwnPropertyDescriptors can reveal it.
“writable false freezes the whole object.”
It locks one data property’s value. Other properties can still be added or changed unless other rules stop them. Freezing and sealing come next in this module.
“configurable false means the value can never change.”
If writable is still true, assignment can still change the value. Configurability controls deletion and descriptor reconfiguration.
“You can unlock a property later.”
configurable: false is one-way. You can tighten a non-configurable writable data property from writable: true to false, but you cannot turn it back on.
“Assignment and defineProperty are interchangeable.”
Assignment can trigger setters and can be blocked by inherited read-only properties. Definition creates or changes an own descriptor directly.
Once a property is non-configurable, JavaScript protects the shape other code may be relying on. The allowed change from writable: true to false only makes the property stricter; it never loosens the contract.
Practice: property descriptors
5 EXERCISESRead the starter code and predict the two console lines. This is the smallest useful descriptor: a value plus flags.
"use strict";
const profile = {};
Object.defineProperty(profile, "id", {
value: 101,
writable: false,
enumerable: true,
configurable: true,
});
try { profile.id = 202; } catch (error) { console.log(error.name); }
console.log(profile.id);The assignment to profile.id fails with TypeError, then the stored value is still 101. That proves the property is read-only, not deleted.
Work out the three console lines without running the code.
"use strict";
const user = {};
Object.defineProperty(user, "token", { value: "abc" });
console.log(Object.keys(user).length);
console.log(user.token);
try { delete user.token; } catch (error) { console.log(error.name); }Object.keys(user).length is 0 because token is non-enumerable. user.token is still abc. Deleting a non-configurable property in strict code throws TypeError.
Use Object.getOwnPropertyDescriptor to inspect a built-in. What two booleans print?
const descriptor = Object.getOwnPropertyDescriptor(Math, "PI");
console.log(descriptor.writable);
console.log(descriptor.configurable);The descriptor reports false for writable and false for configurable. That is why strict code cannot replace or delete Math.PI.
Which property name does Object.keys(settings) show?
const settings = { theme: "dark" };
Object.defineProperty(settings, "token", {
value: "abc",
enumerable: false,
});
console.log(Object.keys(settings));
console.log(settings.token);const settings = { theme: "dark" };
Object.defineProperty(settings, "token", {
value: "abc",
enumerable: false,
});
console.log(Object.keys(settings));
console.log(settings.token);theme was created by a literal, so it is enumerable. token was defined with enumerable: false, so Object.keys(settings) shows only theme, while settings.token still reads abc.
What does the inherited setter log, and why does the definition line not log again?
"use strict";
const log = [];
const proto = { set name(value) { log.push(value); } };
const child = Object.create(proto);
child.name = "Ada";
Object.defineProperty(child, "name", { value: "Own Ada", enumerable: true });
console.log(log.join(","));
console.log(child.name);The setter logs only Ada, from the assignment line. The later defineProperty call does not call the setter; it installs an own name value on child.
Quiz: check your descriptor instincts
7 QUESTIONSQuestion 1 of 7Which call returns the descriptor for one own property?
Choose an answer to see the explanation.
Question 2 of 7What does the descriptor inspection print?
Read the code, then predictconst box = { x: 1 }; console.log(Object.getOwnPropertyDescriptor(box, "x").writable);Choose an answer to see the explanation.
Question 3 of 7What does Object.keys report for a hidden property?
Read the code, then predictconst box = {}; Object.defineProperty(box, "x", { value: 1 }); console.log(Object.keys(box).length);Choose an answer to see the explanation.
Question 4 of 7Which flag decides whether
delete obj.xmay remove a property?Choose an answer to see the explanation.
Question 5 of 7What prints when strict assignment hits an inherited read-only property?
Read the code, then predict"use strict"; const parent = {}; Object.defineProperty(parent, "x", { value: 1, writable: false }); const child = Object.create(parent); try { child.x = 2; } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 6 of 7Which statement about
configurable: falseis correct?Choose an answer to see the explanation.
Question 7 of 7What prints when definition bypasses an inherited setter?
Read the code, then predictconst log = []; const proto = { set name(value) { log.push(value); } }; const child = Object.create(proto); Object.defineProperty(child, "name", { value: "Ada" }); console.log(log.length);Choose an answer to see the explanation.
Key takeaways
- A property descriptor is the value plus hidden flags for an own property.
writablecontrols assignment to a data property’s value.enumerablecontrols whether common listing tools see the property.configurablecontrols deletion and most future descriptor changes.- Literal and assignment defaults are permissive;
definePropertydefaults omitted flags to false. - Assignment uses normal property access. Definition installs an own descriptor directly.
One-liner.
A descriptor is the hidden settings panel that tells JavaScript whether a property can be changed, listed, deleted, or reconfigured.
Up next: Getters & setters.