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) andconfigurable(can it be deleted or reconfigured). - Reading:
Object.getOwnPropertyDescriptorandObject.getOwnPropertyDescriptors. - Writing:
Object.definePropertyandObject.defineProperties. - An ordinary property has every flag
true, while a property created throughdefinePropertygetsfalsefor every flag you omit.
Quick example
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:
- A data property stores a concrete value in the
valuefield. - An accessor property defines
getandsetfunctions.
For a data property:
{
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:
{
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
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 changeLet us check enumerability and configurability:
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:
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:
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
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); // 30From 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
- Creating read-only properties:
Object.defineProperty(config, 'API_KEY', {
value: '12345',
writable: false
});- Hidden properties that stay out of iteration and of
JSON.stringify:
Object.defineProperty(obj, '_id', {
value: 42,
enumerable: false
});-
Implementing reactivity: Vue 2, for example, used
getandsetinside descriptors to track data changes. -
Emulating private fields (before the
#syntax existed):
Object.defineProperty(obj, 'secret', {
value: 'hidden',
enumerable: false
});A summary of the fields:
| Descriptor field | What it is for |
|---|---|
value | the value itself |
writable | whether value can be changed |
enumerable | whether it is visible when iterating |
configurable | whether it can be deleted or redefined |
get | the getter function |
set | the setter function |
Common mistakes
- Expecting omitted flags to be
true. InObject.definePropertyeverything you do not specify isfalse, so the property unexpectedly becomes non writable and non enumerable. - Confusing
writable: falsewith deep immutability. Only the reference itself is locked: the fields of the object it points to can still be changed. - Combining
valuewithgetorsetin one descriptor. That is an immediateTypeError, a property is either data or an accessor. - Not noticing the silent failure. Without
'use strict', writing to awritable: falseproperty does nothing and throws nothing, which looks like a mysterious bug. - Setting
configurable: falsetoo early. There is no way back: such a property can no longer be reconfigured or deleted. - Assuming
Object.assignor spread carry descriptors over. They copy only the values of own enumerable properties, and getters are executed in the process. To preserve descriptors useObject.defineProperties(target, Object.getOwnPropertyDescriptors(source)). - Forgetting that
enumerable: falsehides a property fromObject.keys,for...inandJSON.stringify, but does not make it private:Object.getOwnPropertyNamesstill reveals it.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.