Overview

Objects, as we know, contain properties. Each of the properties of an object has three configuration flags, which can take the values true or false, in addition to the value. These flags are called descriptors:

  • writable - whether the property is writable;
  • enumerable - whether the property is visible during enumerations (e.g. in a for..in loop);
  • configurable - whether the property is available for reconfiguration.

When we create an object property in the "normal way", these three flags are set to true.

The static method Object.defineProperty() is used to change descriptor values, and Object.getOwnPropertyDescriptors() is used to read values.

In other words, descriptors are key-value pairs that describe the behaviour of an object property when performing operations on it (e.g., reading or writing).

Image description

There is a set of descriptors attached to each property of the object under the bonnet.

Example

Let's create an object and add the OS property for the laptop to it. Let's do it using descriptors and static method Object.defineProperty().

Passing it to the method:

  • Object to which we add the property;
  • Property name with a string;
  • Object with descriptor values and a value key containing the property value.
const laptop = {}

Object.defineProperty(laptop, 'os', {
  value: 'MacOS',
  writable: false,
  enumerable: true,
  configurable: true
})

The os property will not be available for overwriting, but will be visible when enumerated and available for reconfiguration.

Let's try to overwrite the os property and display the result:

laptop.os = 'Windows'
console.log(laptop) // { 'os': 'MacOS' }

How to Spell

Object.defineProperty(object, property, descriptors)

The function accepts the following parameters:

  • object - object whose property we are modifying or adding;
  • property - the property for which you want to apply the descriptor;
  • descriptors - a descriptor describing the behaviour of the property.

If the property already exists, Object.defineProperty() will update the flags.
If the property does not exist, the method creates a new property with the specified value and flags. If any flag is not explicitly specified, it is assigned the value false.

How to Realise

The descriptors we can pass to Object.defineProperty() come in two types - data descriptor and access descriptor. Each type of descriptor has its own set of properties.

In both types, the common properties configurable and enumerable can be used.

The descriptor passed to Object.defineProperty() can only be one type of descriptor. It cannot be both at the same time! If you pass to Object.defineProperty() an object containing both data descriptor and access descriptor properties, the method will throw the Invalid property descriptor error. Cannot both specify accessors and a value or writable attribute.

Data Descriptor

A data descriptor is a descriptor that defines the value of a property and the ability to change that value.

  • value - property value, defaults to undefined.
  • writable - whether the value can be changed using an assignment operator.

value

The value property of the data descriptor is responsible for the value of the object property.

Let's add a "Screen Size" property to the notebook:

Object.defineProperty(laptop, 'displaySize', {
  value: '17'
})

Let's derive the obtained data:

const descriptor = Object.getOwnPropertyDescriptor(laptop, 'displaySize')

console.log(descriptor)

We did not specify the other properties explicitly, so the descriptor has the following values:

{
  "value": "17",
  "writable": false,
  "enumerable": false,
  "configurable": false
}

writable

The writable property of the descriptor determines whether the value of the property can be changed using an assignment operator. It is set to false by default for properties created via Object.defineProperty() and to true if the property is added via ..

Let's change the value of writable:

const laptop = {}

Object.defineProperty(laptop, 'displaySize', {
    value: '17',
    writable: false, // Not rewritable.
    configurable: true,
    enumerable: true
})

laptop.displaySize = '18'

console.log(laptop.displaySize) // { 'displaySize': '17' }

In strict mode, we'll get a TypeError, which says we can't change a non-overwritable property.

Access Descriptor

An access descriptor is a descriptor that defines the operation of a property through the property's read and write functions (getter and setter).

get - function used to get the value of a property, returns a value or undefined.

set - a function used to set the value of the property. It takes as its only argument the new value assigned to the property.

🧐 You can see that the two types of descriptors conflict with each other. They describe the same thing - the operation of a property, but they do it in different ways. That's why you can't use a descriptor with properties of both types.

Let's compare a simple object with a name field and an object with a name getter created via Object.defineProperty():

const animal = { _hiddenName : 'Cat' }

Object.defineProperty(animal, 'name', {
    get: function() { return this._hiddenName }
})

const animal2 = {
  name: "And there's a cat in here, too",
}

console.log(animal.name) // Cat
console.log(animal2.name) // And there's a cat in here, too

Both objects have the same behaviour. It is only worth mentioning that the property in the first case is followed by a function that is called automatically. It is enough to write animal.name.

If we need to change the value of the name property, we do animal.name = 'Grey Cat', nothing will happen. The reason is that there is no setter function associated with the name key, so it is not possible to set a value for this property.

Add a setter:

const animal = { _hiddenName : 'Cat' }

Object.defineProperty(animal, 'name', {
    get: function() { return this._hiddenName },
    set: function(value){ this._hiddenName = value }
})

animal.name = 'Dog'
console.log(animal.name) // Dog

Essentially, we can regulate the ability to read and retrieve the value of a property, just as we can in a data descriptor, only more subtly. This approach is used frequently, so we came up with a syntax for declaring getters and setters without calling Object.defineProperty():

const animal = {
  get name() {
    return this._name
  },
  set name(value) {
    this._name = value
  }
}

console.log(animal.name) // undefined

animal.name = 'Cat'
console.log(animal.name) // Cat

Setters may be needed, for example, to modify a value when writing properties. In the example below, we modify the date and write it in the desired format.

const updatedAt = {
  get date() {
    return this._date
  },

  set date(value) {
    this._date = new Intl.DateTimeFormat('en-US').format(value)
  }
}

Let's write the date and time in the date field:

updatedAt.date = new Date(2025, 11, 12)
console.log(updatedAt.date) // 12/12/2025

And we get the date in the correct format: 12/12/2025.

Properties with access methods give us all the data processing capabilities of functions and the simplicity characteristic of working with ordinary properties.

General Properties

Common properties can be specified in both types of descriptors.

enumerable

Defines whether the object property to be created is visible in enumerations.

Let's create two properties on the laptop object - one will be enumerated and one will not:

const laptop = {}

Object.defineProperty(laptop, 'processor',
    // Let's make the processor enumerable as usual:
    { enumerable: true, value: 'Intel Core' }
)

Object.defineProperty(laptop, 'touchID',
    // Make `touchID` NOT enumerated:
    { enumerable: false, value: true }
)

console.log(laptop.touchID) // true
console.log(('touchID' in laptop)) // true
console.log(laptop.hasOwnProperty('touchID')) // true

for (let key in laptop) {
  console.log(key, laptop[key])
} // 'processor': 'Intel Core'

Notice that laptop.touchID exists and has a value, but does not appear in the for..in loop (yet, it exists if you use the in operator). "Enumerable" means: "will be counted by going through the object's properties by enumerating"."

configurable

The configurable property determines whether the object property being created is available for reconfiguration.

Let's change the configurable value:

const laptop = {}

Object.defineProperty(laptop, 'processor', {
    value: 'Intel Core',
    writable: true,
    configurable: false, // Prohibit reconfiguration.
    enumerable: true
})

console.log(laptop.processor) // Intel Core
laptop.processor = 'M1'
console.log(laptop.processor) // 'M1'

Object.defineProperty(laptop, 'processor', {
    value: 'M1 TOP',
    writable: true,
    configurable: true,
    enumerable: true
}) // TypeError: Cannot redefine property: processor.

Attempting to rewrite the processor property descriptor results in a TypeError, even if you are not in strict mode.

☝️ Be careful, changing configurable to false is irreversible and cannot be undone.

If the property is already configurable: false, writable can be changed from true to false without error, but not back to true if it is already false.

And also configurable: false prevents you from being able to use the delete operator to delete an existing property. No error will occur, but the property will not be deleted:

delete laptop.processor
console.log(laptop) // { processor: 'M1' }

From time to time, a developer needs to protect objects from tampering. It is easy to change an object property by mistake. To protect objects from such changes and manage their immunity, we propose to use descriptors such as writable and configurable, setters, and the methods Object.preventExtensions(), Object.seal(), and Object.freeze() to restrict access to the entire object.

Support ❤️

It took a lot of time and effort to create this material. If you found this article useful or interesting, please support my work with a small donation. It will help me to continue sharing my knowledge and ideas.

Make a contribution or Subscription to the author's content: Buy me a Coffee, Patreon, PayPal.