TypeScript 23 ๐ท Getters, Setters, and Accessors
Getters and setters are methods that look like properties. You define get name() and set name(value), and callers use obj.name โ no parentheses, no explicit function call. Under the hood, the getter or setter runs. TypeScript adds type checking on both sides: the getter’s return type and the setter’s parameter type. They’re the tool for computed properties, validation on assignment, lazy initialization, and encapsulation โ hiding internal state behind a property interface that looks simple but does work when accessed.
Key point: A getter looks like a property read but runs code. A setter looks like a property write but runs code. Together they let you expose a clean property interface over internal logic. The class’s public API says user.fullName, but the implementation computes it from firstName and lastName. TypeScript checks the types on both sides. Use getters for derived values, setters for validation, and both when you want the property to feel simple while doing real work.
What getters and setters are
A getter is a method declared with get that runs when a property is read. A setter is declared with set and runs when a property is written.
class Circle {
constructor(private _radius: number) {}
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value < 0) throw new Error('Radius must be positive');
this._radius = value;
}
}
Usage:
const c = new Circle(5);
c.radius; // 5 โ runs the getter
c.radius = 10; // runs the setter
c.radius = -1; // throws
The caller uses c.radius like a plain property. Behind the scenes, the getter or setter runs.
What you get:
- Property syntax for the caller โ no method calls
- Full logic in the getter or setter โ validation, computation, side effects
- Type checking on both read and write
- Encapsulation โ the underlying storage (
_radius) stays private
Conventions:
_nameโ underscore prefix for the private backing field, common in TypeScript and JavaScript#nameโ JavaScript’s real private fields, an alternativereadonlyโ used for properties that have a getter but no setter
Getter-only: A getter without a setter makes the property read-only from the outside.
class User {
constructor(private _name: string) {}
get name(): string {
return this._name;
}
}
const u = new User('Alice');
u.name; // โ
u.name = 'Bob'; // โ read-only
TypeScript flags the assignment as an error โ name has no setter.
Setter-only: A setter without a getter is unusual but allowed โ write-only.
class Secret {
set password(value: string) {
// hash and store
}
}
In practice, setter-only is rare. Almost always you have both or getter-only.
Why getters and setters exist: They let a property look simple while doing real work. Without them, you’d expose a method (
getFullName()) or a raw property that any code could set. With them, you get property syntax and full control over what happens. That balance is what makes them useful.
Types on getters and setters
TypeScript types the getter’s return and the setter’s parameter. They must match.
class Temperature {
private _celsius = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
this._celsius = value;
}
}
The getter returns number; the setter accepts number. TypeScript checks both.
Mismatched types are an error:
class Bad {
private _value = 0;
get value(): number {
return this._value;
}
set value(v: string) { // โ setter parameter must match getter return
this._value = Number(v);
}
}
The error: “Getter and setter must have the same type.”
Why the rule: A property has one type. Reading gives a value; writing takes a value. If they differ, the property’s type is ambiguous. TypeScript enforces consistency.
Type inference: If you specify the getter’s return type, the setter’s parameter must match. If neither is specified, TypeScript infers from the backing field.
class User {
private _name = '';
get name() { return this._name; } // inferred string
set name(v: string) { this._name = v; } // must be string
}
Union types: The property can have a union type.
class Field {
private _value: string | number = '';
get value(): string | number {
return this._value;
}
set value(v: string | number) {
this._value = v;
}
}
Why getter/setter types must match: A property is one thing โ a slot with a type. Reading returns
T; writing acceptsT. If they could differ,obj.x = 'a'; const y: number = obj.x;would be confusing โ what type isx? TypeScript keeps them consistent to make properties predictable.
Computed properties
A computed property derives its value from other fields. Getters are the standard way to expose them.
class User {
constructor(
public firstName: string,
public lastName: string
) {}
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
const u = new User('Alice', 'Johnson');
u.fullName; // "Alice Johnson"
fullName isn’t stored โ it’s computed on read. The caller sees a property; the class computes it.
Why computed properties matter:
- Single source of truth โ
firstNameandlastNameare stored;fullNamederives from them - No stale data โ change
firstNameandfullNameupdates automatically - Clean API โ callers read
fullNamewithout knowing how it’s computed
Read-only by convention: A computed property usually has no setter. If you want to set it, you’d need logic to parse the value โ often a bad idea.
class User {
get fullName(): string { return `${this.firstName} ${this.lastName}`; }
set fullName(value: string) {
const [first, ...rest] = value.split(' ');
this.firstName = first;
this.lastName = rest.join(' ');
}
}
The setter parses "Alice Johnson" into first and last names. It works but is fragile โ “Alice van der Berg” is ambiguous. Prefer read-only computed properties.
Performance: A getter runs every time it’s accessed. If the computation is expensive, consider caching.
class Data {
private _cached: number | null = null;
get expensive(): number {
return this._cached ??= this.computeExpensive();
}
private computeExpensive(): number {
// heavy work
return 42;
}
}
The ??= caches the result on first access. This is a common pattern โ a lazy computed property.
Why computed properties are idiomatic: They keep a single source of truth and derive everything else. No need to keep multiple fields in sync. The alternative โ storing
fullNameas its own field and updating it wheneverfirstNamechanges โ is error-prone. Derived values should be computed, not stored.
Validation on assignment
Setters are where you validate before storing.
class User {
private _age = 0;
get age(): number {
return this._age;
}
set age(value: number) {
if (!Number.isInteger(value)) {
throw new Error('Age must be an integer');
}
if (value < 0 || value > 150) {
throw new Error('Age out of range');
}
this._age = value;
}
}
const u = new User();
u.age = 30; // โ
u.age = -1; // โ throws
u.age = 3.14; // โ throws
The setter enforces the rules. Any assignment goes through it โ there’s no way to bypass.
Validation patterns:
- Range checks โ number within bounds
- Format checks โ email, phone, etc.
- Null checks โ non-null, non-empty
- Cross-field โ value must be consistent with other fields
Throwing vs clamping vs ignoring: Three approaches.
// Throw
set age(v: number) {
if (v < 0) throw new Error('Negative age');
this._age = v;
}
// Clamp
set age(v: number) {
this._age = Math.max(0, Math.min(150, v));
}
// Ignore invalid
set age(v: number) {
if (v < 0) return;
this._age = v;
}
Throwing is loud but forces callers to handle errors. Clamping is quiet but hides bugs. Ignoring is the worst โ silently drops data. Prefer throwing or clamping deliberately.
Setters with side effects: A setter can trigger other work.
class Form {
private _dirty = false;
private _name = '';
get name() { return this._name; }
set name(v: string) {
this._name = v;
this._dirty = true; // mark form dirty
}
get isDirty() { return this._dirty; }
}
Assigning form.name = 'x' sets the field and marks the form dirty. The side effect is invisible to the caller โ that’s the point.
Warning about side effects: Heavy side effects in setters can surprise readers. Document them.
Why setters are good for validation: They’re the single point where a value enters the object. Putting validation there means every assignment is checked. Without a setter, external code could assign an invalid value directly. Setters close that door.
Lazy initialization
A getter can defer expensive initialization until the value is needed.
class Database {
private _connection: Connection | null = null;
get connection(): Connection {
if (!this._connection) {
this._connection = this.openConnection();
}
return this._connection;
}
private openConnection(): Connection {
// expensive setup
return new Connection();
}
}
const db = new Database();
// No connection yet
db.connection; // opens the connection
db.connection; // returns cached
The connection opens on first access. Subsequent accesses return the cached value.
Pattern with nullish coalescing assignment:
get connection(): Connection {
return this._connection ??= this.openConnection();
}
??= assigns only if _connection is null or undefined. Concise and clear.
Lazy + validation: The getter computes on demand; the setter can invalidate the cache.
class Data {
private _cached: number | null = null;
private _input = 0;
get input() { return this._input; }
set input(v: number) {
this._input = v;
this._cached = null; // invalidate cache
}
get computed(): number {
return this._cached ??= this._input * 2;
}
}
Setting input clears the cache, so the next read of computed recomputes. That’s the standard lazy-with-invalidation pattern.
Why lazy init matters: Some resources are expensive to create but may not be needed. Opening a database connection, loading a config file, initializing a heavy object โ a getter can defer these until the first access. That’s lazy initialization.
Why getters are the natural fit for lazy init: The getter runs on read. If the value isn’t needed, the getter never runs. If it is, the getter creates and caches it. The caller just reads a property and gets the value โ the lazy behavior is invisible.
Getters and setters with readonly
A getter-only property can’t be assigned. That’s effectively readonly from the outside.
class User {
constructor(private _id: string) {}
get id(): string {
return this._id;
}
}
const u = new User('u-1');
u.id; // โ
"u-1"
u.id = 'u-2'; // โ read-only
The _id is private; only the getter exposes it. No setter means no external assignment.
Compare with readonly:
class User {
readonly id: string;
constructor(id: string) { this.id = id; }
}
readonly prevents assignment after construction but leaves the property publicly readable. A getter-only property does the same โ with the option to add logic or compute the value.
When to use which:
| Need | Use |
|---|---|
| Simple immutable field | readonly |
| Computed or hidden field | Getter |
| Both read and write | Getter + setter |
| Internal-only | private or #private |
readonly and getters together:
class Config {
constructor(private readonly _apiUrl: string) {}
get apiUrl(): string {
return this._apiUrl;
}
}
The backing field is readonly (can’t be reassigned even internally); the getter exposes it. Belt and suspenders.
Why getter-only instead of
readonly: When the value is computed, not stored, or when you want to hide the storage entirely.readonly id: stringexposes both the field and its name. A getter-only property exposes only the read interface โ you can rename the field, change its type, or compute it, without breaking callers.
Getters, setters, and interfaces
Interfaces can declare properties that classes implement with getters and setters.
interface Named {
name: string;
}
class User implements Named {
private _name = '';
get name(): string { return this._name; }
set name(v: string) { this._name = v; }
}
Named requires a name property. User provides it via getter and setter. From the interface’s perspective, it’s just a property.
Read-only in an interface:
interface Identified {
readonly id: string;
}
class User implements Identified {
get id(): string { return 'u-1'; }
}
readonly in the interface means no setter. A class with just a getter satisfies it.
Write-only is not expressible: Interfaces can’t declare write-only properties. If you need one, use a method.
Why this matters: Getter/setter properties are just properties from an interface’s perspective. The interface doesn’t care how they’re implemented โ the shape is what counts. This lets you swap implementations (getter, plain field, computed) without changing the interface.
Why interfaces treat getters as properties: Structural typing only sees the shape. A
name: stringin an interface is satisfied by a plain field, a getter, or a class property. The consumer doesn’t know or care which. That’s the flexibility structural typing gives you.
A full example
A bank account with validation, computed properties, and lazy initialization.
class BankAccount {
private _balance = 0;
private _history: string[] | null = null;
constructor(
private readonly _accountNumber: string,
private _owner: string
) {}
// Read-only computed
get accountNumber(): string {
return this._accountNumber;
}
// Read-only computed with formatting
get maskedAccount(): string {
return `****${this._accountNumber.slice(-4)}`;
}
// Read-only computed
get formattedBalance(): string {
return `$${this._balance.toFixed(2)}`;
}
// Read-write with validation
get owner(): string {
return this._owner;
}
set owner(value: string) {
const trimmed = value.trim();
if (trimmed.length === 0) {
throw new Error('Owner name cannot be empty');
}
this._owner = trimmed;
}
// Read-only balance
get balance(): number {
return this._balance;
}
// Lazy history
get history(): readonly string[] {
return this._history ??= [];
}
// Methods that mutate balance and history
deposit(amount: number): void {
if (amount <= 0) throw new Error('Deposit must be positive');
this._balance += amount;
this.history.push(`+${amount}`);
}
withdraw(amount: number): void {
if (amount <= 0) throw new Error('Withdrawal must be positive');
if (amount > this._balance) throw new Error('Insufficient funds');
this._balance -= amount;
this.history.push(`-${amount}`);
}
}
// Usage
const acct = new BankAccount('1234567890', 'Alice');
acct.deposit(100);
acct.deposit(50);
acct.withdraw(30);
console.log(acct.maskedAccount); // ****7890
console.log(acct.formattedBalance); // $120.00
console.log(acct.history); // ['+100', '+50', '-30']
acct.owner = ' Alice Smith ';
console.log(acct.owner); // 'Alice Smith'
// acct.balance = 1000; // โ read-only
// acct.owner = ''; // โ throws
What this shows:
- Read-only computed properties โ
maskedAccount,formattedBalance,balance - Read-write with validation โ
owner - Lazy initialization โ
historycreated on first access - Private backing fields โ
_balance,_owner,_accountNumber,_history - Methods that mutate โ
deposit,withdraw; the balance is read-only from outside
The caller uses properties (acct.balance, acct.owner = 'x') without knowing about the logic behind them.
Why this shape: It’s a realistic class with a clean property interface. Computed values are derived; validation happens on assignment; the internal history is lazily created. The public API is simple โ properties โ while the implementation does real work.
Complete Example Session
# ============================================
# PART 1: BASIC GETTER AND SETTER
# ============================================
cat > basic.ts << 'EOF'
class Circle {
constructor(private _radius: number) {}
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value < 0) throw new Error('Radius must be positive');
this._radius = value;
}
get area(): number {
return Math.PI * this._radius ** 2;
}
}
const c = new Circle(5);
console.log(c.radius);
console.log(c.area.toFixed(2));
c.radius = 10;
console.log(c.area.toFixed(2));
try {
c.radius = -1;
} catch (e) {
console.log((e as Error).message);
}
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: READ-ONLY PROPERTY
# ============================================
cat > readonly.ts << 'EOF'
class User {
constructor(private _id: string) {}
get id(): string { return this._id; }
}
const u = new User('u-1');
console.log(u.id);
// u.id = 'u-2'; // โ read-only
EOF
npx tsc --noEmit readonly.ts
# (no errors)
# ============================================
# PART 3: TYPE MISMATCH ERROR
# ============================================
cat > mismatch.ts << 'EOF'
class Bad {
private _value = 0;
get value(): number { return this._value; }
set value(v: string) { this._value = Number(v); } // โ
}
EOF
npx tsc --noEmit mismatch.ts
# [ mismatch.ts:5:3 - 'get' and 'set' accessor must have the same type. ]
rm mismatch.ts
# ============================================
# PART 4: LAZY INITIALIZATION
# ============================================
cat > lazy.ts << 'EOF'
class Data {
private _cached: number | null = null;
private _input = 0;
get input() { return this._input; }
set input(v: number) {
this._input = v;
this._cached = null;
}
get computed(): number {
return this._cached ??= this._input * 2;
}
}
const d = new Data();
console.log(d.computed); // 0
d.input = 21;
console.log(d.computed); // 42
console.log(d.computed); // 42 (cached)
EOF
npx tsc --noEmit lazy.ts
# (no errors)
# ============================================
# PART 5: INTERFACE COMPATIBILITY
# ============================================
cat > iface.ts << 'EOF'
interface Named {
name: string;
}
interface Identified {
readonly id: string;
}
class User implements Named, Identified {
private _name = '';
constructor(private _id: string) {}
get name() { return this._name; }
set name(v: string) { this._name = v; }
get id() { return this._id; }
}
const u = new User('u-1');
u.name = 'Alice';
console.log(u.name, u.id);
EOF
npx tsc --noEmit iface.ts
# (no errors)
# ============================================
# PART 6: BANK ACCOUNT
# ============================================
cat > bank.ts << 'EOF'
class BankAccount {
private _balance = 0;
private _history: string[] | null = null;
constructor(
private readonly _accountNumber: string,
private _owner: string
) {}
get accountNumber() { return this._accountNumber; }
get maskedAccount() { return `****${this._accountNumber.slice(-4)}`; }
get formattedBalance() { return `$${this._balance.toFixed(2)}`; }
get balance() { return this._balance; }
get history(): readonly string[] { return this._history ??= []; }
get owner() { return this._owner; }
set owner(v: string) {
const t = v.trim();
if (t.length === 0) throw new Error('Empty owner');
this._owner = t;
}
deposit(amount: number): void {
if (amount <= 0) throw new Error('Invalid deposit');
this._balance += amount;
this.history.push(`+${amount}`);
}
withdraw(amount: number): void {
if (amount <= 0) throw new Error('Invalid withdrawal');
if (amount > this._balance) throw new Error('Insufficient');
this._balance -= amount;
this.history.push(`-${amount}`);
}
}
const acct = new BankAccount('1234567890', 'Alice');
acct.deposit(100);
acct.deposit(50);
acct.withdraw(30);
console.log(acct.maskedAccount);
console.log(acct.formattedBalance);
console.log(acct.history);
EOF
npx tsc --noEmit bank.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts readonly.ts lazy.ts iface.ts bank.ts
node basic.js
# [ 5 ]
# [ 78.54 ]
# [ 314.16 ]
# [ Radius must be positive ]
node readonly.js
# [ u-1 ]
node lazy.js
# [ 0 ]
# [ 42 ]
# [ 42 ]
node iface.js
# [ Alice u-1 ]
node bank.js
# [ ****7890 ]
# [ $120.00 ]
# [ [ '+100', '+50', '-30' ] ]
Quick Reference
Syntax
| Form | Example |
|---|---|
| Getter | get name(): string { } |
| Setter | set name(v: string) { } |
| Getter only | get name(): string { } |
| Setter only | set password(v: string) { } |
| Private backing | private _name = '' |
| JS private backing | #name = '' |
Usage
| Operation | Syntax |
|---|---|
| Read | obj.name |
| Write | obj.name = 'x' |
| Read-only | obj.name (write fails) |
| In class | this.name / this.name = 'x' |
Types
| Rule | Detail |
|---|---|
| Getter return | T |
| Setter parameter | T (must match) |
| Mismatch | Compile error |
| Inference | From backing field |
| Union | Allowed |
Access Modifiers
| Modifier | Effect |
|---|---|
public get | Publicly readable |
private get | Class-only read |
protected get | Class + subclasses |
static get | On the class |
| No setter | Read-only from outside |
Common Patterns
| Pattern | Purpose |
|---|---|
| Computed | Derive from fields |
| Validated | Enforce on write |
| Lazy | Cache on first read |
| Read-only | Expose without setter |
| Hidden backing | _name or #name |
| Side effect | Mark dirty, notify |
Computed vs Stored
| Aspect | Computed | Stored |
|---|---|---|
| Source | Derived on read | Field |
| Staleness | Never | Can drift |
| Performance | Recomputes | Fast read |
| Example | fullName | firstName |
Lazy Initialization
| Form | Example |
|---|---|
| If check | if (!this._x) this._x = ... |
| Nullish assign | this._x ??= ... |
| With invalidation | Setter clears cache |
Validation in Setters
| Approach | Behavior |
|---|---|
| Throw | Error on invalid |
| Clamp | Adjust to valid range |
| Ignore | Silently drop |
| Log | Record and continue |
Getter vs Method
| Aspect | Getter | Method |
|---|---|---|
| Syntax | obj.x | obj.x() |
| Purpose | Property-like | Action-like |
| Side effects | Should be none | Allowed |
| Async | โ | โ |
Interface Compatibility
| Interface | Class |
|---|---|
name: string | Getter + setter |
readonly id: string | Getter only |
| Method | Method |
| Write-only | Not expressible |
Error Cases
| Error | Cause |
|---|---|
| Type mismatch | Getter and setter differ |
| Read-only | No setter defined |
| Recursion | Getter calls itself |
| Access before init | Getter uses unset field |
readonly vs Getter-Only
| Aspect | readonly | Getter-only |
|---|---|---|
| Field exposed | โ | โ |
| Logic | โ | โ |
| Computed | โ | โ |
| Simple | โ | โ ๏ธ |
| Renameable | โ | โ |
Best Practices
โ Do This:
// Use getters for computed properties
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
} // โ
// Use setters for validation
set age(value: number) {
if (value < 0) throw new Error('Invalid');
this._age = value;
} // โ
// Keep getter and setter types matching
get value(): number { return this._value; }
set value(v: number) { this._value = v; } // โ
// Use private backing fields
private _name = ''; // โ
// Lazy initialize expensive values
get connection(): Connection {
return this._connection ??= this.open();
} // โ
// Make computed properties read-only
get total(): number { return this.subtotal + this.tax; } // โ
// Invalidate caches in setters
set input(v: number) {
this._input = v;
this._cached = null;
} // โ
// Throw on invalid input in setters
set email(v: string) {
if (!v.includes('@')) throw new Error('Invalid email');
this._email = v;
} // โ
โ Don’t Do This:
// Don't do heavy work in getters without caching
get data(): number[] {
return expensiveComputation(); // runs every read // โ ๏ธ
}
// Don't have side effects in getters
get user(): User {
this.logAccess(); // โ ๏ธ side effect on read // โ ๏ธ
return this._user;
}
// Don't mismatch getter/setter types
get value(): number { return this._value; }
set value(v: string) { } // โ error // โ
// Don't expose the backing field publicly
public _name = ''; // โ ๏ธ defeats the purpose // โ ๏ธ
// Don't forget to invalidate caches
get computed(): number { return this._cached!; }
set input(v: number) { this._input = v; } // โ ๏ธ cache stale // โ ๏ธ
// Don't do async work in getters
async get data() { } // โ not valid // โ
// Don't use getters for expensive I/O
get file(): string {
return fs.readFileSync('big.txt', 'utf8'); // โ ๏ธ // โ ๏ธ
}
// Don't overload setters with logic
set x(v: number) {
this.x = v; // โ infinite recursion // โ
}
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Recursive getter | Infinite loop | Use backing field |
| Type mismatch | Compile error | Match getter/setter types |
| Expensive getter | Runs every read | Cache the result |
| Side effects in getter | Surprising behavior | Move to method |
| Forget cache invalidation | Stale data | Clear in setter |
| Public backing field | Defeats encapsulation | Use private |
| Async getter | Not supported | Use a method |
| Getter with I/O | Blocks reads | Load separately |
| Setter silent drop | Hides bugs | Throw on invalid |
| Overriding getter only | Confusing | Override both or neither |
Real-World Examples
1. Basic getter
get name(): string { return this._name; }
2. Basic setter
set name(v: string) { this._name = v; }
3. Computed full name
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
4. Read-only property
get id(): string { return this._id; }
5. Validated setter
set age(v: number) {
if (v < 0) throw new Error('Negative');
this._age = v;
}
6. Clamping setter
set percent(v: number) {
this._percent = Math.max(0, Math.min(100, v));
}
7. Lazy connection
get connection(): Connection {
return this._conn ??= this.connect();
}
8. Lazy with invalidation
get total(): number {
return this._total ??= this.compute();
}
set items(v: Item[]) {
this._items = v;
this._total = null;
}
9. Private backing field
private _value = 0;
get value() { return this._value; }
10. JavaScript private field
#value = 0;
get value() { return this.#value; }
11. Static getter
static get version(): string { return '1.0.0'; }
12. Getter returning readonly
get items(): readonly Item[] { return this._items; }
13. Setter with trim
set name(v: string) {
this._name = v.trim();
}
14. Setter marking dirty
set value(v: string) {
this._value = v;
this._dirty = true;
}
15. Getter computing from array
get count(): number { return this._items.length; }
16. Getter with formatting
get displayPrice(): string {
return `$${this._price.toFixed(2)}`;
}
17. Interface with readonly
interface Identified {
readonly id: string;
}
18. Class implementing both
class User implements Named, Identified {
get name() { return this._name; }
set name(v: string) { this._name = v; }
get id() { return this._id; }
}
19. Setter with cross-field validation
set endDate(v: Date) {
if (v < this._startDate) throw new Error('End before start');
this._endDate = v;
}
20. Getter for formatted output
get summary(): string {
return `${this.name}: ${this.count} items`;
}
Visual: Getter and Setter Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Caller โ
โ โ
โ const r = circle.radius; โ
โ โ โ
โ โผ โ
โ get radius() { return this._radius; } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Caller โ
โ โ
โ circle.radius = 10; โ
โ โ โ
โ โผ โ
โ set radius(v) { โ
โ if (v < 0) throw ...; โ
โ this._radius = v; โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Computed Property
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Stored fields: โ
โ โ firstName = 'Alice' โ
โ โ lastName = 'Johnson' โ
โ โ
โ Computed getter: โ
โ get fullName() { โ
โ return `${firstName} ${lastName}`; โ
โ } โ
โ โ
โ Access: โ
โ user.fullName โ 'Alice Johnson' โ
โ โ
โ Change firstName: โ
โ user.firstName = 'Bob' โ
โ user.fullName โ 'Bob Johnson' โ
โ โ
โ No stale data โ computed on read โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Validation Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ user.age = -5 โ
โ โ โ
โ โผ โ
โ set age(v: number) { โ
โ if (v < 0) throw new Error('...'); โ
โ this._age = v; โ
โ } โ
โ โ โ
โ โผ โ
โ โ Error thrown โ value not stored โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ user.age = 30 โ
โ โ โ
โ โผ โ
โ set age(v: number) { โ
โ if (v < 0) throw ...; โ
โ this._age = v; โ
โ } โ
โ โ โ
โ โผ โ
โ โ
_age = 30 โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Lazy Initialization
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ First access: โ
โ โ
โ db.connection โ
โ โ โ
โ โผ โ
โ get connection() { โ
โ return this._conn ??= this.open(); โ
โ } โ
โ โ โ
โ โผ โ
โ _conn === null โ open() runs โ cached โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Subsequent access: โ
โ โ
โ db.connection โ
โ โ โ
โ โผ โ
โ _conn already set โ returned directly โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: readonly vs Getter-Only
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ readonly id: string โ
โ โ
โ โ Public field โ
โ โ Assignable only in constructor โ
โ โ Simple โ
โ โ Field name exposed โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ get id(): string โ
โ โ
โ โ Computed or hidden โ
โ โ Backing field private โ
โ โ Renameable โ
โ โ Can add logic โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Cache Invalidation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Data { โ
โ private _input = 0; โ
โ private _cached: number | null = null; โ
โ โ
โ get input() { return this._input; } โ
โ โ
โ set input(v: number) { โ
โ this._input = v; โ
โ this._cached = null; โ invalidate โ
โ } โ
โ โ
โ get computed(): number { โ
โ return this._cached ??= this._input * 2;โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Sequence: โ
โ โ
โ d.input = 21 โ invalidate cache โ
โ d.computed โ recompute โ 42 โ cache โ
โ d.computed โ return cached โ 42 โ
โ d.input = 10 โ invalidate cache โ
โ d.computed โ recompute โ 20 โ cache โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Common Error โ Recursion
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Infinite recursion โ
โ โ
โ get name() { โ
โ return this.name; // calls itself โ
โ } โ
โ โ
โ โ Stack overflow โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
Correct โ
โ โ
โ private _name = ''; โ
โ โ
โ get name() { โ
โ return this._name; // backing field โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Interface Compatibility
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ interface Named { name: string; } โ
โ โ
โ Satisfied by: โ
โ โ
โ โ public name = '' โ
โ โ get name() + set name() โ
โ โ get name() only (if readonly) โ
โ โ
โ Any of these works โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ interface Identified { readonly id: string; }โ
โ โ
โ Satisfied by: โ
โ โ
โ โ readonly id: string โ
โ โ get id() only โ
โ โ
โ No setter required โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Is the value derived from other fields? โ
โ โ โ
โ โโโ Yes โโโบ Getter (read-only) โ
โ โ โ
โ โโโ No โ
โ โ โ
โ โโโ Need validation on write? โ
โ โ โ โ
โ โ โโโ Yes โโโบ Getter+Setter โ
โ โ โ โ
โ โ โโโ No โโโบ Plain field โ
โ โ โ
โ โโโ Need lazy init? โ
โ โ โ
โ โโโ Yes โโโบ Getter with โ
โ cache โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Property Access Comparison
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Plain field โ
โ โ
โ class C { name = ''; } โ
โ โ
โ c.name; โ direct read โ
โ c.name = 'x'; โ direct write โ
โ โ
โ No logic, no validation โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Getter + setter โ
โ โ
โ class C { โ
โ private _name = ''; โ
โ get name() { return this._name; } โ
โ set name(v: string) { this._name = v; } โ
โ } โ
โ โ
โ c.name; โ runs getter โ
โ c.name = 'x'; โ runs setter โ
โ โ
โ Full logic, validation, computed values โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Getter | get name() โ runs on read |
| Setter | set name(v) โ runs on write |
| Computed property | Derived from other fields |
| Read-only | Getter without setter |
| Validation | Setter enforces rules |
| Lazy initialization | Compute on first read |
| Cache invalidation | Setter clears cached value |
| Backing field | _name or #name โ private storage |
| Type match | Getter and setter must have same type |
Key takeaways:
- Getters run on property read; setters run on property write
- Callers use property syntax โ
obj.x, notobj.x() - Getter and setter types must match โ TypeScript enforces it
- Computed properties derive values from other fields โ no stale data
- Setters validate โ the single point where values enter
- Lazy initialization โ defer expensive work until first access
- Cache invalidation โ clear the cache in the setter that changes the input
- Getter-only makes a property read-only from outside
- Use private backing fields (
_nameor#name) to hide storage - Interfaces treat getters as plain properties โ no special syntax
- Avoid heavy work in getters without caching โ they run on every read
- Avoid side effects in getters โ reads should be pure
- Setters can have side effects โ marking dirty, notifying, invalidating
Remember: Getters and setters let a property look simple while doing real work. Use getters for computed values, lazy initialization, and read-only exposure. Use setters for validation, cache invalidation, and marking state. Keep them typed consistently, avoid heavy work on read, and don’t create infinite loops by returning the property itself. When the property is genuinely simple, a plain field is better โ getters and setters are for when the property needs to behave like more than a slot.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!