Skip to main content

Property descriptor

A property descriptor is an internal object that describes how an object's property behaves: whether it can be changed, enumerated or deleted, and whether it has a getter or a setter. Put simply, it is metadata about the property: not only its value, but also the flags that define how you are allowed to work with it.

Theory

TL;DR

  • A descriptor describes not the value, but the rules for working with a property.
  • Properties come in two types: a data property (value, writable) and an accessor property (get, set).
  • Flags common to both: enumerable (is it visible when iterating) and configurable (can it be deleted or reconfigured).
  • Reading: Object.getOwnPropertyDescriptor and Object.getOwnPropertyDescriptors.
  • Writing: Object.defineProperty and Object.defineProperties.
  • An ordinary property has every flag true, while a property created through defineProperty gets false for every flag you omit.

Quick example

javascript
const user = { name: 'Tim' }; const descriptor = Object.getOwnPropertyDescriptor(user, 'name'); console.log(descriptor); // { // value: 'Tim', // writable: true, // enumerable: true, // configurable: true // }

All of these flags default to true when the property is created the ordinary way.

Kinds of properties and the shape of a descriptor

Properties come in two types:

  1. A data property stores a concrete value in the value field.
  2. An accessor property defines get and set functions.

For a data property:

javascript
{ value: any, // the value writable: boolean, // can value be changed enumerable: boolean, // is it visible when iterating (for...in, Object.keys) configurable: boolean // can it be deleted or its descriptor changed }

For an accessor property:

javascript
{ get: function | undefined, // getter set: function | undefined, // setter enumerable: boolean, configurable: boolean }

A single descriptor cannot carry value or writable together with get or set: it is either data or an accessor.

Defining a property by hand

javascript
const user = {}; Object.defineProperty(user, 'role', { value: 'admin', writable: false, // the value cannot be changed enumerable: false, // does not show up when iterating configurable: false // cannot be deleted or reconfigured }); console.log(user.role); // 'admin' user.role = 'user'; console.log(user.role); // 'admin', the value did not change

Let us check enumerability and configurability:

javascript
const user = {}; Object.defineProperty(user, 'name', { value: 'Tim', enumerable: false, configurable: false }); console.log(Object.keys(user)); // [], not enumerated delete user.name; // will not be deleted console.log(user.name); // 'Tim'

In sloppy mode such attempts are silently ignored, while under 'use strict' they throw a TypeError.

Defining many properties at once:

javascript
const user = {}; Object.defineProperties(user, { name: { value: 'Alex', writable: true, enumerable: true }, age: { value: 25, writable: false } });

Getting every descriptor of an object in one go:

javascript
const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyDescriptors(obj)); // { // a: { value: 1, writable: true, enumerable: true, configurable: true }, // b: { value: 2, writable: true, enumerable: true, configurable: true } // }

A getter and a setter in a descriptor

javascript
const person = {}; let _age = 25; Object.defineProperty(person, 'age', { get() { return _age; }, set(value) { if (value < 0) throw new Error('Age cannot be negative'); _age = value; }, enumerable: true, configurable: true }); console.log(person.age); // 25 person.age = 30; // calls the setter console.log(person.age); // 30

From the outside person.age looks like an ordinary property, but every read and write goes through your functions, and that is exactly where validation belongs.

Real world uses

  1. Creating read-only properties:
javascript
Object.defineProperty(config, 'API_KEY', { value: '12345', writable: false });
  1. Hidden properties that stay out of iteration and of JSON.stringify:
javascript
Object.defineProperty(obj, '_id', { value: 42, enumerable: false });
  1. Implementing reactivity: Vue 2, for example, used get and set inside descriptors to track data changes.

  2. Emulating private fields (before the # syntax existed):

javascript
Object.defineProperty(obj, 'secret', { value: 'hidden', enumerable: false });

A summary of the fields:

Descriptor fieldWhat it is for
valuethe value itself
writablewhether value can be changed
enumerablewhether it is visible when iterating
configurablewhether it can be deleted or redefined
getthe getter function
setthe setter function

Common mistakes

  • Expecting omitted flags to be true. In Object.defineProperty everything you do not specify is false, so the property unexpectedly becomes non writable and non enumerable.
  • Confusing writable: false with deep immutability. Only the reference itself is locked: the fields of the object it points to can still be changed.
  • Combining value with get or set in one descriptor. That is an immediate TypeError, a property is either data or an accessor.
  • Not noticing the silent failure. Without 'use strict', writing to a writable: false property does nothing and throws nothing, which looks like a mysterious bug.
  • Setting configurable: false too early. There is no way back: such a property can no longer be reconfigured or deleted.
  • Assuming Object.assign or spread carry descriptors over. They copy only the values of own enumerable properties, and getters are executed in the process. To preserve descriptors use Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)).
  • Forgetting that enumerable: false hides a property from Object.keys, for...in and JSON.stringify, but does not make it private: Object.getOwnPropertyNames still reveals it.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.