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.
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;
}
}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.
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.
class Box {
accessor width = 10;
static accessor count = 0;
}
const b = new Box();
b.width = 20;
Box.count++;Try it yourself
- 1
Call it the right way
In the first example, change
gen.create();toIdGenerator.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, thenID-. - 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 whatt.fahrenheitis after it.No error. The property now has a getter and a setter, so assignment works and
t.fahrenheitbecomes41. - 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
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 functionA 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 getterThe ^? 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
42An 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
1Break 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.
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 writetoJSON. Anaccessorfield'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. - ▸
staticblocks 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#privatestatics that are declared above it. - ▸
A class's static side is inherited:
class Child extends IdGeneratorcan callChild.create(), andthisinside a static method is the class it was called on. Beware ofthis.#nextin a static method:Child.create()then runs withthisequal toChild, which has no#next, and throwsTypeError: Cannot read private member #next from an object whose class did not declare it. WritingIdGenerator.#next(the class name) avoids that, as the example does.
Remember this
- 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 bereadonly,private,protectedand#private, just like instance members. - 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
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 atry/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
A getter (
get fahrenheit() { ... }) and setter (set fahrenheit(v) { ... }) are methods that are used like fields:t.fahrenheitcalls the getter,t.fahrenheit = 212calls 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 isTS2540, and at runtime it throwsCannot set property celsius of #<Temperature> which has only a getter. - 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(): stringwithset value(input: string | number)lets callers assign numbers but always read strings back. Readingf.valuegivesstring, assigning acceptsstring | number, and assigning abooleanis an error. Hovering the property shows the write type. - 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 withstatic accessor. Use it when you plan to attach a decorator; otherwise a normal field is simpler. - 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 byObject.keysor copied by{ ...obj }, which can surprise you when you spread a class instance.
Explain it without notes
What is the difference between an instance member and a static member?
What do getters and setters give you over plain fields?
Can a getter and setter for the same property have different types?
What does the accessor keyword do, and when would you use it?
Practice
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.
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
accessordoes and why decorators want it.