accessor

Declares an auto-accessor: a get/set pair over a hidden private backing field, mainly for decorators.

Signature
class C {
  accessor propertyName: Type = initializer;
  static accessor staticProperty: Type;
  accessor #privateProperty: Type;
}
  • Added in TypeScript 4.9

Parameters

ParameterTypeDescription
$propertyNameidentifier | #private nameThe public name of the generated getter/setter pair. A hidden private field of a compiler-generated name holds the value.
$Typeoptionaltype annotationThe property type. It is used for both the getter's return type and the setter's parameter type.
$initializeroptionalTypeOptional 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

Example · typescript
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.

Example · typescript
// 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.

Example · typescript
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.

Example · typescript
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

  • Be the first to comment on this lesson.