JavaScript

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 ignored

In 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 directly

The 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 there

There'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: true

This 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 deleting

Practical 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.defineProperty to 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.