accessor
Declares an auto-accessor: a get/set pair over a hidden private backing field, mainly for decorators.
class C {
accessor propertyName: Type = initializer;
static accessor staticProperty: Type;
accessor #privateProperty: Type;
}- Added in TypeScript 4.9
Parameters
| Parameter | Type | Description |
|---|---|---|
$propertyName | identifier | #private name | The public name of the generated getter/setter pair. A hidden private field of a compiler-generated name holds the value. |
$Typeoptional | type annotation | The property type. It is used for both the getter's return type and the setter's parameter type. |
$initializeroptional | Type | Optional initial value, assigned to the backing field when the instance is constructed. |
Errors
Combining the modifier with a plain field declaration of the same name, or reading the backing field from outside the class, is an error; at runtime the generated backing field behaves like any #private field and throws a TypeError on a foreign receiver.
Changelog
- PHP 4.9Auto-accessors added as support for an upcoming ECMAScript feature from the decorators proposal. They de-sugar to a private backing field plus get and set.
- PHP 5.0Standard decorators can be applied to auto-accessors, receiving a ClassAccessorDecoratorContext and a { get, set } pair — the use case the syntax exists for.
Writing accessor x: string is shorthand for a private backing field plus a matching getter and setter. The de-sugaring is exactly what the handbook shows: a # field holding the value, get x() returning it, set x(value) assigning it.
When to reach for it
Almost always: because a decorator needs somewhere to hook. A standard decorator on a plain field can only transform the initial value; it can never see later writes. On an auto-accessor it receives the get and set functions and can replace both, which is how observable properties, change tracking, validation and dependency-tracking reactivity libraries are built. Outside of decorators the syntax buys you very little over a normal field.
What trips people up
The property lands on the prototype as a getter/setter pair, not on the instance. That changes observable behaviour: it is not an own enumerable property, so Object.keys(instance) and JSON.stringify(instance) skip it, and code that copies objects with spread loses it. It is also not a free upgrade from a field — a subclass that redeclares the same name as a plain field will shadow the accessor pair with an own data property. Reach for it when a decorator needs the hook, and keep plain fields otherwise.
Example
class Person {
accessor name: string = "Ada";
}
// what `accessor name` de-sugars to:
class PersonDesugared {
#name: string = "Ada";
get name(): string { return this.#name; }
set name(value: string) { this.#name = value; }
}
const a = new Person();
const b = new PersonDesugared();
console.log(a.name, b.name);
a.name = "Grace";
b.name = "Grace";
console.log(a.name, b.name);
const desc = Object.getOwnPropertyDescriptor(PersonDesugared.prototype, "name");
console.log("accessor pair on prototype:", typeof desc?.get, typeof desc?.set);When to use it
- Give a change-tracking or validation decorator a place to intercept every read and write of a property.
- Expose a property publicly while keeping the value in a genuinely private backing field, without writing the pair by hand.
- Build an observable model class where a decorator notifies subscribers on assignment.
Frequently asked questions
What does the accessor keyword do in TypeScript?
It declares an auto-accessor — a getter and setter pair on the prototype backed by a compiler-generated private field, instead of a plain instance field. It was added in TypeScript 4.9 as part of the standard decorators proposal.
When should I use accessor instead of a normal class field?
Use it when a decorator has to intercept reads and writes of the property; that is what it is for. A standard decorator on a plain field only sees the initial value, whereas on an auto-accessor it receives the get and set functions and can replace both.
Why doesn't my accessor property show up in JSON.stringify?
Because the property is a prototype getter/setter pair, not an own enumerable property, and JSON.stringify only serialises own enumerable properties. Add a toJSON() method that returns a plain object, or keep plain class fields for data you intend to serialise.
More examples
What it de-sugars to (runnable)
Prints Ada, then Grace, then "accessor pair on prototype: function function". This is exactly what the compiler emits for `accessor name`: a private field plus a getter/setter pair on the prototype. Writing it out by hand is the portable form, and is what to reach for until engines ship auto-accessors natively.
// The de-sugared equivalent — this half DOES run anywhere.
class Person {
#name: string = "Ada";
get name(): string { return this.#name; }
set name(value: string) { this.#name = value; }
}
const p = new Person();
console.log(p.name);
p.name = "Grace";
console.log(p.name);
const desc = Object.getOwnPropertyDescriptor(Person.prototype, "name");
console.log("accessor pair on prototype:", typeof desc?.get, typeof desc?.set);The reason it exists: a decorator that sees every write
Prints "write theme <- dark", then "read theme -> dark", then "final: dark". The decorator returns a replacement { get, set } pair, so every access goes through it. A decorator on a plain `theme = "light"` field could only have rewritten the initial value; the assignment on the next line would have been invisible to it.
function logged<T, V>(
target: ClassAccessorDecoratorTarget<T, V>,
context: ClassAccessorDecoratorContext<T, V>
): ClassAccessorDecoratorResult<T, V> {
const name = String(context.name);
return {
get(this: T) {
const value = target.get.call(this);
console.log(`read ${name} -> ${value}`);
return value;
},
set(this: T, value: V) {
console.log(`write ${name} <- ${value}`);
target.set.call(this, value);
},
};
}
class Settings {
@logged accessor theme: string = "light";
}
const s = new Settings();
s.theme = "dark";
console.log("final:", s.theme);Static and private auto-accessors
Prints "production", then "s3****", then an empty own-keys array and "{}". The last two lines are the gotcha in action: nothing about this class is an own enumerable property, so a model built out of auto-accessors round-trips through JSON as an empty object unless you add a toJSON method.
class Config {
static accessor environment: string = "development";
accessor #secret: string = "s3cr3t";
reveal(): string {
// the private auto-accessor is reachable only inside the class body
return this.#secret.slice(0, 2) + "****";
}
}
Config.environment = "production";
console.log(Config.environment);
console.log(new Config().reveal());
const instance = new Config();
console.log("own keys:", Object.keys(instance));
console.log("serialised:", JSON.stringify(instance));
Discussion