What are JavaScript object property flags and descriptors?
The short answer
Every property on a JavaScript object has hidden metadata called property flags that control how the property behaves. There are three flags: writable (can the value be changed?), enumerable (does it show up in loops?), and configurable (can the property be deleted or its flags modified?). Together with the property's value, these flags form what's called a property descriptor.
When you create a property the normal way — obj.name = 'Alice' — all three flags default to true. But JavaScript gives you fine-grained tools to inspect and change them.
Inspecting property descriptors
You can look at any property's descriptor using Object.getOwnPropertyDescriptor:
const person = { name: 'Alice', age: 30 };
console.log(
Object.getOwnPropertyDescriptor(person, 'name')
);
// { value: 'Alice', writable: true, enumerable: true, configurable: true }If you want to see descriptors for every property at once, use Object.getOwnPropertyDescriptors(person).
The three flags
writable
Controls whether the property's value can be reassigned. When set to false, the property becomes read-only:
const config = {};
Object.defineProperty(config, 'apiUrl', {
value: 'https://api.example.com',
writable: false,
enumerable: true,
configurable: true,
});
config.apiUrl = 'https://evil.com';
console.log(config.apiUrl); // 'https://api.example.com' — silently ignoredIn strict mode, trying to write to a non-writable property throws a TypeError. In sloppy mode, the assignment silently fails.
enumerable
Controls whether the property shows up in for...in loops, Object.keys(), and Object.assign(). Setting it to false effectively hides the property from most iteration methods:
const user = { name: 'Alice' };
Object.defineProperty(user, 'internalId', {
value: 'abc-123',
enumerable: false,
});
console.log(Object.keys(user)); // ['name'] — internalId is hidden
console.log(user.internalId); // 'abc-123' — still accessible directlyThe property still exists and you can access it if you know the name. It just doesn't advertise itself. This is how built-in methods like toString work — they're there, but they don't clutter your for...in loops.
configurable
Controls whether the property can be deleted with delete and whether its descriptor can be modified further. Once you set configurable to false, it's a one-way door — you can't set it back to true.
const settings = {};
Object.defineProperty(settings, 'mode', {
value: 'production',
writable: false,
configurable: false,
});
delete settings.mode; // fails silently (TypeError in strict mode)
console.log(settings.mode); // 'production' — still thereThere's one exception: even with configurable: false, you can still change writable from true to false (but not back). This lets you lock things down progressively.
Defining properties with Object.defineProperty
A subtle but important detail: when you create a property with Object.defineProperty, any flags you don't specify default to false. This is the opposite of normal assignment, where they default to true:
const obj = {};
Object.defineProperty(obj, 'hidden', { value: 42 });
// writable: false, enumerable: false, configurable: false
obj.visible = 42;
// writable: true, enumerable: true, configurable: trueThis catches people off guard. With Object.defineProperty, you're opting in to each behavior. With regular assignment, you get full access by default.
Data descriptors vs accessor descriptors
Everything above describes data descriptors — they have a value and a writable flag. But there's a second kind called accessor descriptors, which use get and set functions instead:
const user = { firstName: 'Alice', lastName: 'Smith' };
Object.defineProperty(user, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
[this.firstName, this.lastName] = value.split(' ');
},
enumerable: true,
configurable: true,
});
console.log(user.fullName); // 'Alice Smith'
user.fullName = 'Bob Jones';
console.log(user.firstName); // 'Bob'A descriptor is either data or accessor — never both. You can't have value and get on the same property. JavaScript throws a TypeError if you try.
Object.freeze and Object.seal
These methods apply property flags in bulk to lock down an entire object:
Object.freeze makes every property non-writable and non-configurable, and prevents new properties from being added. The object becomes effectively immutable (though only shallowly — nested objects aren't frozen).
Object.seal makes every property non-configurable and prevents adding or removing properties, but existing writable properties can still be changed.
const frozen = Object.freeze({ a: 1, b: 2 });
frozen.a = 99; // silently fails (TypeError in strict mode)
const sealed = Object.seal({ a: 1, b: 2 });
sealed.a = 99; // works — value can still change
sealed.c = 3; // silently fails — no new properties
delete sealed.a; // silently fails — no deletingPractical use cases
You might wonder when you'd reach for property descriptors in real code:
- Library internals — making properties non-enumerable so they don't pollute
Object.keys()or spread operations. - Constants — creating truly read-only properties that can't be reassigned even accidentally.
- Framework reactivity — Vue 2's reactivity system used
Object.definePropertyto intercept gets and sets on data properties. - Protecting shared state — freezing configuration objects so consuming code can't accidentally mutate them.
Why interviewers ask this
Property flags and descriptors reveal whether you understand JavaScript objects beyond the surface level. Most developers never think about what happens beneath obj.key = value, but this knowledge explains why certain built-in properties behave the way they do, how frameworks implement reactivity, and how to write more robust code.