Getters & Setters
Getters and setters let you intercept property reads and writes on a class. Instead of exposing a raw field you expose a computed property backed by custom logic — validation, lazy evaluation, change tracking, and more.
TypeScript inherits the get / set accessor syntax from JavaScript but adds static type checking so the types of the getter return value and the setter parameter are verified at compile time.
Basic syntax
Prefix a method with get to create a getter, and set to create a setter. From the outside they look exactly like a plain property — no parentheses, no special call syntax.
class Temperature {
private _celsius: number = 0;
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5 / 9;
}
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) {
throw new RangeError('Temperature below absolute zero');
}
this._celsius = value;
}
}
const t = new Temperature();
t.celsius = 100;
console.log(t.fahrenheit); // 212
t.fahrenheit = 32;
console.log(t.celsius); // 0t.celsius and t.fahrenheit as plain properties. The accessor syntax is completely transparent to the outside world.Read-only getters (no setter)
A getter without a corresponding setter produces a read-only virtual property. TypeScript raises a compile-time error if you attempt to assign to it.
class Circle {
constructor(private _radius: number) {}
get radius(): number { return this._radius; }
get area(): number {
return Math.PI * this._radius ** 2;
}
get circumference(): number {
return 2 * Math.PI * this._radius;
}
}
const c = new Circle(5);
console.log(c.area.toFixed(2)); // 78.54
console.log(c.circumference.toFixed(2)); // 31.42
// c.area = 100;
// Error: Cannot assign to 'area' because it is a read-only property.Validation in setters
Setters are the natural home for validation logic. They act as a firewall between external code and internal state, ensuring the object is always in a valid, consistent state.
class User {
private _email = '';
private _age = 0;
get email(): string { return this._email; }
set email(value: string) {
const trimmed = value.trim();
if (!trimmed.includes('@') || !trimmed.includes('.')) {
throw new TypeError(`Invalid email: "${value}"`);
}
this._email = trimmed.toLowerCase();
}
get age(): number { return this._age; }
set age(value: number) {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new RangeError(`Age must be 0–150, got ${value}`);
}
this._age = value;
}
}
const user = new User();
user.email = ' Hello@Example.COM ';
console.log(user.email); // 'hello@example.com'
user.age = 25;
// user.age = -1; // RangeError: Age must be 0–150, got -1Lazy / cached computation
Getters are recomputed on every access. For expensive calculations you can memoize — compute once, cache the result, invalidate the cache when the underlying data changes.
class Document {
private _content: string;
private _wordCount: number | null = null;
constructor(content: string) {
this._content = content;
}
get wordCount(): number {
if (this._wordCount === null) {
console.log('(computing word count…)');
this._wordCount = this._content
.trim()
.split(/\s+/)
.filter(Boolean).length;
}
return this._wordCount;
}
set content(value: string) {
this._content = value;
this._wordCount = null; // invalidate cache
}
}
const doc = new Document('Hello world foo bar');
console.log(doc.wordCount); // (computing…) → 4
console.log(doc.wordCount); // → 4 (no recomputation)
doc.content = 'New content here';
console.log(doc.wordCount); // (computing…) → 3Getters in interfaces
Interfaces describe getters as readonly properties. Any class that exposes the property — whether via a getter or a plain readonly field — satisfies the contract.
interface Shape {
readonly area: number;
readonly perimeter: number;
describe(): string;
}
class Rectangle implements Shape {
constructor(private w: number, private h: number) {}
get area(): number { return this.w * this.h; }
get perimeter(): number { return 2 * (this.w + this.h); }
describe(): string {
return `Rectangle ${this.w}x${this.h}: area=${this.area}, perimeter=${this.perimeter}`;
}
}
class Square implements Shape {
constructor(private s: number) {}
get area(): number { return this.s ** 2; }
get perimeter(): number { return 4 * this.s; }
describe(): string {
return `Square ${this.s}x${this.s}: area=${this.area}, perimeter=${this.perimeter}`;
}
}
const shapes: Shape[] = [new Rectangle(4, 6), new Square(5)];
shapes.forEach(s => console.log(s.describe()));Overriding accessors in subclasses
class Animal {
private _name: string;
constructor(name: string) { this._name = name; }
get name(): string { return this._name; }
set name(v: string) { this._name = v.trim(); }
}
class Dog extends Animal {
override get name(): string { return `Dog: ${super.name}`; }
// Must also keep the setter when overriding the getter
override set name(v: string) { super.name = v; }
}
const d = new Dog('Rex');
console.log(d.name); // Dog: Rex
d.name = 'Buddy';
console.log(d.name); // Dog: BuddyThe auto-accessor shorthand (TypeScript 4.9+)
TypeScript 4.9 introduced the accessor keyword, which auto-generates a private backing field plus a getter/setter pair. It produces identical runtime behaviour to the manual version but with less boilerplate — and is especially useful when combined with decorators.
// Manual (pre-4.9 style)
class OldStyle {
private _x = 0;
get x() { return this._x; }
set x(v: number) { this._x = v; }
}
// auto-accessor shorthand (4.9+)
class NewStyle {
accessor x: number = 0;
}
// Both compile to equivalent JavaScript.
// 'accessor' is particularly useful with decorators:
function logged(target: ClassAccessorDecoratorTarget<unknown, number>,
ctx: ClassAccessorDecoratorContext) {
return {
get(this: unknown) { const v = target.get.call(this); console.log('get', v); return v; },
set(this: unknown, v: number) { console.log('set', v); target.set.call(this, v); },
};
}
class Counter {
@logged accessor count = 0;
}
const counter = new Counter();
counter.count = 5; // logs: set 5
console.log(counter.count); // logs: get 5 → 5Getters vs methods — choosing the right tool
Scenario | Getter | Method |
|---|---|---|
Derived value, no parameters | ✅ preferred | — |
Expensive computation (cacheable) | ✅ + memoize | — |
Operation needs arguments | — | ✅ required |
Side effects (API call, I/O) | — | ✅ clearer intent |
Validation on write | ✅ setter | — |
Change tracking / reactivity | ✅ setter | — |
Common pitfalls
Infinite recursion: writing
this.name = valueinside the setter fornamecalls the setter again. Always write to the backing field:this._name = value.Forgetting to declare the private backing field — the getter/setter has nowhere to store the value.
Mismatched getter/setter types. TypeScript allows them to differ but it is almost always a mistake; keep them consistent.
Expensive logic in getters called in tight loops — profile before deciding to memoize.