Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~21 min· topic 6 of 8

Topic 6.6

Static Members, Getters/Setters and accessor

In one line

static members belong to the class itself and not to its objects. Getters and setters make a method look like a field, and the accessor keyword creates a field with a hidden getter and setter that decorators can hook into.

Think of it like this

A school. Each student has their own name and marks (that belongs to the student). The school also has things that belong to the whole school, not to any student: the founding year, the number of students enrolled so far, the bell schedule. Instance members are the students' things; static members are the school's things. And a getter is like a report card line "Grade: A" that looks like a fact but is worked out from the marks every time you ask.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

static
A member that belongs to the class itself and is reached through the class name.
Instance member
A field or method that every object built from the class has its own copy or access to.
Factory method
A static method that builds and returns an instance, hiding the constructor details.
Static block
A static { } block that runs once when the class is defined.
Getter
A method called when you read a property, written get name() { }.
Setter
A method called when you assign to a property, written set name(v) { }.
Computed property
A value worked out from other data each time it is read, usually through a getter.
accessor
A keyword for a field that is backed by an automatic getter and setter, so decorators can wrap it.
Write type
The type a setter accepts, which may differ from the type the getter returns.

Step by step

01Statics live on the class

IdGenerator has a private counter, a constant and a registry, all static. The static block pushes the first entry. create() is a static method that uses them.

You call IdGenerator.create(). You never make an IdGenerator object just to use it.

id-generator.tswhole filets
class IdGenerator {
  static #next = 1;
  static readonly PREFIX = "ID-";
  static registry: string[] = [];

  static {
    IdGenerator.registry.push("boot");
  }

  static create() {
    const id = IdGenerator.PREFIX + IdGenerator.#next++;
    IdGenerator.registry.push(id);
    return id;
  }
}
Statics live on the classdiagram
Rendering diagram…

02A getter and setter pair

The real value is a #celsius field. fahrenheit is a computed property: reading it converts, assigning to it converts back. celsius has only a getter, so it is read-only.

temperature.tswhole filets
class Temperature {
  #celsius = 0;

  get fahrenheit(): number {
    return this.#celsius * 1.8 + 32;
  }
  set fahrenheit(value: number) {
    this.#celsius = (value - 32) / 1.8;
  }

  get celsius() {
    return this.#celsius;
  }
}

const t = new Temperature();
t.fahrenheit = 212;
t.celsius;       // 100
t.celsius = 5;   // error TS2540: read-only (only a getter)

03Different read and write types

Let callers pass a string or a number but always read a clean string. The getter says string, the setter parameter says string | number. The panel for f.value shows the write type, and assigning a boolean is rejected.

04accessor: a field you can intercept

accessor width = 10; reads and writes like a field. Behind the scenes there is a hidden private slot with a generated getter and setter. Plain use is identical; its payoff arrives with decorators in Topic 6.8.

box.tswhole filets
class Box {
  accessor width = 10;
  static accessor count = 0;
}
const b = new Box();
b.width = 20;
Box.count++;

Try it yourself

  1. 1

    Call it the right way

    In the first example, change gen.create(); to IdGenerator.create();. Predict the Types panel and the console output.

    Clean, and the console prints ID-1 ID-2, then the registry with a third id, then ID-.

  2. 2

    Add a setter to celsius

    In the Temperature example add set celsius(value: number) { this.#celsius = value; } next to the getter. Predict whether the last line is still an error, and what t.fahrenheit is after it.

    No error. The property now has a getter and a setter, so assignment works and t.fahrenheit becomes 41.

  3. 3

    Reject strings in the setter

    In the Field example change the setter parameter to input: number. Which lines become errors? Read the Types panel.

Code & diagrams

static fields, a static block and a factory TypeScript 4.4+

Statics are shared by the whole class. The compiler even suggests the fix for gen.create(), which also throws when the program runs, so the last console line never prints.

Console

(nothing was printed)

Expected console output

ID-1 ID-2
[ 'boot', 'ID-1', 'ID-2' ]
Uncaught TypeError: gen.create is not a function
Getter and setter, and a read-only property

A property with only a getter is read-only. TypeScript reports it, and the runtime throws if you ignore the warning, so the program stops at the last line.

Console

(nothing was printed)

Expected console output

100 212
Uncaught TypeError: Cannot set property celsius of #<Temperature> which has only a getter
A setter that accepts more than the getter returns

The ^? hovers the property in the assignment, so it shows the write type. Reading f.value gives a string (the const s line is clean). Assigning true is rejected, because the setter takes string or number.

Console

(nothing was printed)

Expected console output

42
The accessor keyword TypeScript 4.9+

An accessor field behaves exactly like a normal field when you use it. The difference is under the hood: it has a getter and setter that Topic 6.8's decorators can replace.

Console

(nothing was printed)

Expected console output

20
1

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Assign to a getter-only property

In the Temperature example keep t.celsius = 5; as the last line.

terminal
$ Types panel
── what you'll see ──
index.ts(20,3): error TS2540: Cannot assign to 'celsius' because it is a read-only property.

Myth vs fact

Myth

A getter is faster or cached.

Fact

It runs its body every time it is read. Compute cheap things only, or cache in a private field yourself.

Myth

static members are copied into every instance.

Fact

They exist once, on the class. Instances don't have them, which is why gen.create is not a function.

Myth

accessor is just another word for getter and setter.

Fact

It declares a field and generates the getter, setter and hidden storage for it. You get a field-like property that a decorator can wrap.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Accessors are properties on the prototype, not own properties of the instance. Object.keys(t) and the spread { ...t } do not include them, and JSON.stringify skips them unless you write toJSON. An accessor field's hidden storage is private, so it is not copied either.

  • ▸

    Getter and setter types may be unrelated, but older versions required the getter type to be assignable to the setter type. Since TypeScript 5.1 they can be completely independent, as long as both have explicit type annotations, which is useful for "parse on set" patterns. Reads in your own code use the getter type; assignments use the setter type. Compound assignments such as f.value += "x" use both.

  • ▸

    static blocks and static initialisers run in the order they appear, top to bottom, at class definition time, so a static that depends on another must come after it. A static block can read #private statics that are declared above it.

  • ▸

    A class's static side is inherited: class Child extends IdGenerator can call Child.create(), and this inside a static method is the class it was called on. Beware of this.#next in a static method: Child.create() then runs with this equal to Child, which has no #next, and throws TypeError: Cannot read private member #next from an object whose class did not declare it. Writing IdGenerator.#next (the class name) avoids that, as the example does.

Remember this

  1. 1

    The problem this solves: Some data and helpers belong to the whole class, such as a shared counter, not to each object. Storing them on every object wastes space and gets out of sync. A member marked **static** lives on the class itself, not on the objects it builds. You reach it with the class name: IdGenerator.create(), IdGenerator.PREFIX. Calling it on an instance is an error (TS2576: Property 'create' does not exist on type 'IdGenerator'. Did you mean to access the static member 'IdGenerator.create' instead?) and would fail at runtime too. Static fields can be readonly, private, protected and #private, just like instance members.

  2. 2

    Typical uses: constants (static readonly PREFIX = "ID-"), factory methods that build instances in a controlled way (User.fromJson(...), often together with a private constructor from Topic 6.2), counters and registries shared by all instances. Statics are shared global state, so they are hard to test; use them sparingly. Inside a class, static members can't use the class's generic type parameters, because there is no single instance type at that level (Topic 7.4 introduces generic classes).

  3. 3

    A static initialization block (static { ... }, TypeScript 4.4) is code that runs once, when the class is defined. It can set up several static fields in order, handle a try/catch, or touch private statics that a simple initializer can't. In the example it adds "boot" to the registry before anything else runs.

  4. 4

    A getter (get fahrenheit() { ... }) and setter (set fahrenheit(v) { ... }) are methods that are used like fields: t.fahrenheit calls the getter, t.fahrenheit = 212 calls the setter with 212. They let you compute values, validate input, or keep the real data in a private field while presenting a nice public name. A property with only a getter is read-only: writing to it is TS2540, and at runtime it throws Cannot set property celsius of #<Temperature> which has only a getter.

  5. 5

    The type of a getter is its return type; the type of the setter is its parameter. The two may be different: get value(): string with set value(input: string | number) lets callers assign numbers but always read strings back. Reading f.value gives string, assigning accepts string | number, and assigning a boolean is an error. Hovering the property shows the write type.

  6. 6

    The **accessor** keyword (TypeScript 4.9) declares a field that the language turns into a private storage slot plus an automatically written getter and setter pair: accessor width = 10; behaves like a field when you use it. Why bother? Because the pair can be intercepted: decorators (Topic 6.8) can wrap the getter and setter, which is impossible for a plain field. It also works with static accessor. Use it when you plan to attach a decorator; otherwise a normal field is simpler.

  7. 7

    Getters run every time they are read, so keep them cheap and free of side effects, and don't return a fresh object from a getter if callers compare it with ===. A getter on the prototype also isn't listed by Object.keys or copied by { ...obj }, which can surprise you when you spread a class instance.

Explain it without notes

01

What is the difference between an instance member and a static member?

02

What do getters and setters give you over plain fields?

03

Can a getter and setter for the same property have different types?

04

What does the accessor keyword do, and when would you use it?

Practice

01

Write a class Config with static readonly defaults = { retries: 3 } and a static method with(overrides: Partial<typeof Config.defaults>) returning a merged object. Call it.

02

Write a class Person with private #first and #last fields, a getter fullName, and a setter that splits a string on the first space.

Trade-offs

  • ↔

    Getters and setters keep a stable public name while you change what is behind it, at the cost of hidden work on every access.

  • ↔

    Static members are convenient, but they are global state that every test shares. Prefer passing dependencies in when behaviour needs to vary.

Done when you can

  • I can declare static fields and methods and call them through the class.

  • I can write a static block and say when it runs.

  • I can write a getter and setter pair and make a read-only property.

  • I can give a getter and a setter different types.

  • I can explain what accessor does and why decorators want it.