| |

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 alternative
  • readonly โ€” 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 accepts T. If they could differ, obj.x = 'a'; const y: number = obj.x; would be confusing โ€” what type is x? 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 โ€” firstName and lastName are stored; fullName derives from them
  • No stale data โ€” change firstName and fullName updates automatically
  • Clean API โ€” callers read fullName without 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 fullName as its own field and updating it whenever firstName changes โ€” 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:

NeedUse
Simple immutable fieldreadonly
Computed or hidden fieldGetter
Both read and writeGetter + setter
Internal-onlyprivate 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: string exposes 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: string in 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 โ€” history created 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

FormExample
Getterget name(): string { }
Setterset name(v: string) { }
Getter onlyget name(): string { }
Setter onlyset password(v: string) { }
Private backingprivate _name = ''
JS private backing#name = ''

Usage

OperationSyntax
Readobj.name
Writeobj.name = 'x'
Read-onlyobj.name (write fails)
In classthis.name / this.name = 'x'

Types

RuleDetail
Getter returnT
Setter parameterT (must match)
MismatchCompile error
InferenceFrom backing field
UnionAllowed

Access Modifiers

ModifierEffect
public getPublicly readable
private getClass-only read
protected getClass + subclasses
static getOn the class
No setterRead-only from outside

Common Patterns

PatternPurpose
ComputedDerive from fields
ValidatedEnforce on write
LazyCache on first read
Read-onlyExpose without setter
Hidden backing_name or #name
Side effectMark dirty, notify

Computed vs Stored

AspectComputedStored
SourceDerived on readField
StalenessNeverCan drift
PerformanceRecomputesFast read
ExamplefullNamefirstName

Lazy Initialization

FormExample
If checkif (!this._x) this._x = ...
Nullish assignthis._x ??= ...
With invalidationSetter clears cache

Validation in Setters

ApproachBehavior
ThrowError on invalid
ClampAdjust to valid range
IgnoreSilently drop
LogRecord and continue

Getter vs Method

AspectGetterMethod
Syntaxobj.xobj.x()
PurposeProperty-likeAction-like
Side effectsShould be noneAllowed
AsyncโŒโœ…

Interface Compatibility

InterfaceClass
name: stringGetter + setter
readonly id: stringGetter only
MethodMethod
Write-onlyNot expressible

Error Cases

ErrorCause
Type mismatchGetter and setter differ
Read-onlyNo setter defined
RecursionGetter calls itself
Access before initGetter uses unset field

readonly vs Getter-Only

AspectreadonlyGetter-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

PitfallProblemSolution
Recursive getterInfinite loopUse backing field
Type mismatchCompile errorMatch getter/setter types
Expensive getterRuns every readCache the result
Side effects in getterSurprising behaviorMove to method
Forget cache invalidationStale dataClear in setter
Public backing fieldDefeats encapsulationUse private
Async getterNot supportedUse a method
Getter with I/OBlocks readsLoad separately
Setter silent dropHides bugsThrow on invalid
Overriding getter onlyConfusingOverride 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

ConceptMeaning
Getterget name() โ€” runs on read
Setterset name(v) โ€” runs on write
Computed propertyDerived from other fields
Read-onlyGetter without setter
ValidationSetter enforces rules
Lazy initializationCompute on first read
Cache invalidationSetter clears cached value
Backing field_name or #name โ€” private storage
Type matchGetter and setter must have same type

Key takeaways:

  • Getters run on property read; setters run on property write
  • Callers use property syntax โ€” obj.x, not obj.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 (_name or #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!