| |

TypeScript 19 ๐Ÿ”ท Classes โ€” Properties, Methods, and Constructors

TypeScript classes start as JavaScript classes โ€” properties, methods, and constructors โ€” and add a type layer on top. Every property, method parameter, and return value can be typed. The compiler checks that the class is used correctly โ€” that properties are initialized, methods are called with the right arguments, and instances match the class’s declared shape. Classes are how you model objects with both state and behavior, and TypeScript makes them safer without changing how JavaScript works.

Key point: A class declares a shape โ€” properties it has, methods it defines, and access rules. TypeScript checks that properties are initialized (under strictPropertyInitialization), that methods use their parameters correctly, and that instances are constructed with the right arguments. The class is still a JavaScript class at runtime; the types are only at compile time.


Declaring a class

A TypeScript class looks like a JavaScript class, with types added.

class User {
  id: number;
  name: string;
  email: string;

  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  greet(): string {
    return `Hello, ${this.name}!`;
  }
}

What’s declared:

  • id, name, email โ€” properties with types
  • constructor(id, name, email) โ€” parameters typed
  • greet(): string โ€” method with return type

Creating an instance:

const alice = new User(1, 'Alice', 'alice@example.com');
alice.greet();                    // 'Hello, Alice!'
alice.id;                         // number
alice.missing;                    // โŒ property doesn't exist

The compiler checks:

  • Constructor called with 3 arguments of the right types
  • Properties exist and have the declared types
  • Methods exist and are called correctly

Property initialization: Under strictPropertyInitialization (part of strict), every property must be initialized โ€” either at declaration or in the constructor.

class User {
  id: number;                     // โœ… assigned in constructor
  name: string;                   // โœ… assigned in constructor
  email: string = 'none';         // โœ… default

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

Without assignment, the compiler errors:

class Bad {
  id: number;                     // โŒ property 'id' has no initializer
}

Why this matters: Uninitialized properties are undefined at runtime โ€” a common source of bugs. TypeScript forces you to initialize every property, or explicitly declare it as possibly undefined.

Why classes still matter: Modern TypeScript often favors plain objects and functions, but classes still shine when you need state with behavior, private state, inheritance, or interface implementation. Classes are the right tool for modeling entities with identity and methods. The type system makes them safer without changing their runtime behavior.


Properties

Properties declare the shape of instances. They can be initialized inline, in the constructor, or with defaults.

Inline initialization:

class Config {
  host = 'localhost';
  port = 8080;
  debug = false;
}

Types are inferred from the defaults: host: string, port: number, debug: boolean.

Explicit types:

class User {
  id: number = 0;
  name: string = '';
  tags: string[] = [];
}

Explicit types are useful when the default doesn’t match the final type, or for documentation.

Constructor initialization:

class User {
  id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

The most common pattern โ€” the constructor sets properties from parameters.

Optional properties:

class User {
  id: number;
  nickname?: string;             // may be undefined

  constructor(id: number) {
    this.id = id;
  }
}

? means the property may be absent. It’s typed string | undefined.

Definite assignment assertion โ€” !:

class User {
  id!: number;                    // assigned elsewhere (e.g., by a framework)
}

The ! tells TypeScript “trust me, this will be set before use.” Use sparingly โ€” it silences a real check.

Readonly properties:

class User {
  readonly id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;                 // โœ… assignable in constructor
    this.name = name;
  }

  changeId(): void {
    // this.id = 2;               // โŒ readonly after construction
  }
}

readonly allows assignment only in the constructor. After that, the property can’t be reassigned.

Static properties:

class User {
  static count = 0;
  static readonly MAX = 100;

  constructor() {
    User.count++;
  }
}

User.count;                       // 0, then increments
User.MAX;                         // 100

Static properties live on the class, not instances. They’re shared across all instances.

Why properties are typed: The type tells you what shape an instance has. The compiler catches missing initialization, wrong types, and access to nonexistent properties. It’s the object shape you’ve seen with interfaces, but for classes.

Why readonly matters: Immutable properties are safer โ€” no accidental reassignment, no shared mutable state. For IDs, creation timestamps, and configuration, readonly documents intent and prevents bugs. It’s shallow: nested objects can still be mutated unless you make them readonly too.


Methods

Methods are functions on instances. They have access to this.

class Calculator {
  value = 0;

  add(n: number): this {
    this.value += n;
    return this;
  }

  subtract(n: number): this {
    this.value -= n;
    return this;
  }

  get(): number {
    return this.value;
  }
}

new Calculator().add(5).subtract(2).get();  // 3

Typed parameters and returns:

class User {
  name = '';

  greet(title: string): string {
    return `Hello, ${title} ${this.name}`;
  }
}

title must be a string; greet returns a string. The compiler enforces both.

The this return type: Returning this preserves the subclass type through method chains.

class Base {
  setName(name: string): this {
    return this;
  }
}

class Child extends Base {
  setAge(age: number): this {
    return this;
  }
}

new Child().setName('x').setAge(1);  // โœ… works

If setName returned Base, .setAge would fail because Base doesn’t have it. this keeps the chain going.

Optional and default parameters:

class Formatter {
  format(value: string, prefix?: string): string {
    return prefix ? `${prefix}${value}` : value;
  }

  pad(value: string, length = 10): string {
    return value.padEnd(length);
  }
}

Same rules as regular functions.

Static methods:

class User {
  static create(name: string): User {
    return new User(name);
  }

  constructor(public name: string) {}
}

User.create('Alice');

Static methods live on the class, not instances. Factory methods are a common pattern.

Overloading methods:

class Parser {
  parse(input: string): object;
  parse(input: number): number;
  parse(input: string | number): object | number {
    return typeof input === 'string' ? {} : input;
  }
}

Same overload syntax as functions.

Private methods: Prefix with private to hide from outside the class.

class User {
  private validate(): boolean {
    return this.name.length > 0;
  }

  save(): void {
    if (!this.validate()) throw new Error('invalid');
  }
}

Private methods can be called only from within the class.

Why method typing matters: Method signatures are the class’s API. The compiler checks every call โ€” right arguments, right return type โ€” and prevents typos and wrong usage. That’s the same benefit as interfaces, but with implementation attached.

Why this return type is useful: It preserves the subclass type through chains, so fluent interfaces work naturally across inheritance. It’s a small feature that makes builder patterns and fluent APIs clean.


Constructors

The constructor initializes instances. TypeScript adds types to parameters and supports several convenient patterns.

Basic constructor:

class User {
  id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

Parameter properties โ€” the shorthand:

class User {
  constructor(
    public id: number,
    public name: string,
    private email: string
  ) {}
}

public, private, protected, and readonly on constructor parameters do two things:

  • Declare a property
  • Assign it automatically from the parameter

The example above is equivalent to declaring id, name, email as properties and assigning them in the constructor. It’s the standard shorthand for simple cases.

Optional constructor parameters:

class User {
  constructor(
    public id: number,
    public nickname?: string
  ) {}
}

new User(1);                       // โœ…
new User(1, 'Al');                 // โœ…

Default constructor parameters:

class Config {
  constructor(
    public host = 'localhost',
    public port = 8080
  ) {}
}

new Config();                      // host=localhost, port=8080

Calling super โ€” inheritance:

class Animal {
  constructor(public name: string) {}
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);                   // must call super first
  }
}

In a derived class, super() must be called before accessing this.

Constructor return type: Constructors can’t declare a return type. They implicitly return the instance.

class User {
  // constructor(): User { }     // โŒ not allowed
  constructor() {}                // โœ…
}

Private constructors: A private constructor prevents new outside the class โ€” useful for singletons or factory-only classes.

class Singleton {
  private static instance: Singleton | null = null;

  private constructor() {}

  static getInstance(): Singleton {
    if (!this.instance) this.instance = new Singleton();
    return this.instance;
  }
}

Singleton.getInstance();           // โœ…
new Singleton();                   // โŒ constructor is private

Why constructor typing matters: It’s the entry point to creating instances. TypeScript checks that new is called with the right arguments, that required parameters are provided, and that super is called appropriately in derived classes.

Why parameter properties are idiomatic: They eliminate boilerplate. constructor(private userService: UserService) declares the property and assigns it in one line. It’s the most common pattern for dependency injection in Angular and NestJS โ€” and it’s how you’ll see constructors written most of the time.


Access modifiers

TypeScript adds public, private, protected, and readonly to control access.

public โ€” the default:

class User {
  public name: string = '';
  // equivalent to:
  name: string = '';
}

Public members are accessible everywhere. That’s the default, so you usually don’t write public.

private โ€” class only:

class User {
  private secret = 'hidden';

  reveal(): string {
    return this.secret;            // โœ… inside class
  }
}

const u = new User();
u.secret;                          // โŒ private

Private members are accessible only from within the class body โ€” not subclasses, not outside.

protected โ€” class and subclasses:

class Animal {
  protected name: string = '';

  getName(): string {
    return this.name;
  }
}

class Dog extends Animal {
  bark(): string {
    return `${this.name} barks`;   // โœ… subclass can access
  }
}

new Animal().name;                 // โŒ protected

Protected members are accessible from the class and any subclass.

readonly:

class User {
  readonly id: number;

  constructor(id: number) {
    this.id = id;
  }
}

Readonly members can be assigned only in the constructor.

Combining modifiers:

class Config {
  private readonly secret: string = 'x';
  protected readonly version: number = 1;
  public readonly name: string = 'config';
}

Modifiers compose: private readonly means both restrictions apply.

private is compile-time only: Unlike JavaScript’s #private fields, TypeScript’s private is erased at runtime. It’s a compile-time check, not a runtime guarantee.

class User {
  private secret = 'x';
}

const u = new User();
(u as any).secret;                 // โœ… compiles โ€” bypasses check

The private keyword prevents access from TypeScript code, but JavaScript can still reach the property.

JavaScript’s #private โ€” runtime private:

class User {
  #secret = 'x';

  reveal(): string {
    return this.#secret;
  }
}

const u = new User();
u.#secret;                         // โŒ syntax error, not just type error

# fields are actually private at runtime โ€” no bypass. TypeScript supports them natively.

Comparison:

Featureprivate#private
Runtime enforcedโŒโœ…
Accessible via as anyโœ…โŒ
TypeScript supportโœ…โœ…
UseConventionalTrue encapsulation

Why access modifiers matter: They express intent and catch mistakes. A private method can’t be called from outside; a protected method is available to subclasses. The compiler enforces the rules. It’s the same idea as interfaces โ€” establishing a contract about what’s part of the public API.

Why #private is the modern choice: It’s actually private at runtime. TypeScript’s private is a compile-time convention โ€” anyone can bypass it with as any. # fields are enforced by the JavaScript engine. For real encapsulation, use #. For convention and tooling, private is fine.


Constructors and dependency injection

The constructor is where dependencies enter a class โ€” the pattern Angular and NestJS rely on.

Constructor injection:

class UserService {
  constructor(private http: HttpClient) {}

  getUser(id: number): Promise<User> {
    return this.http.get(`/users/${id}`);
  }
}

The parameter property private http: HttpClient declares and assigns the dependency. The class doesn’t create the HttpClient โ€” it receives one.

Why this pattern: Testable, flexible, explicit. You can pass a real HttpClient in production and a mock in tests. The class doesn’t know or care which โ€” it just uses what it was given.

Multiple dependencies:

class OrderService {
  constructor(
    private http: HttpClient,
    private logger: Logger,
    private config: Config
  ) {}

  async placeOrder(order: Order): Promise<void> {
    this.logger.info('Placing order');
    await this.http.post('/orders', order);
  }
}

Framework integration: In Angular, decorators like @Injectable() handle the wiring. In NestJS, @Injectable() and constructor injection are the standard pattern. In plain TypeScript, you pass the dependencies manually.

Without a framework:

const http = new HttpClient();
const logger = new Logger();
const orderService = new OrderService(http, logger, config);

With a DI container: Frameworks like InversifyJS provide the container that instantiates and wires classes.

Why constructor injection is idiomatic: It’s explicit, testable, and works everywhere. No global state, no service locator, no magic โ€” just parameters. The class declares what it needs, and whoever creates it provides those dependencies.

Why DI is important in large codebases: When classes create their own dependencies (this.http = new HttpClient()), they’re coupled to specific implementations and hard to test. Constructor injection decouples them โ€” the class depends on an interface, and the caller decides what to pass. That’s the foundation of testable, maintainable code.


A full example

A class hierarchy modeling users and admins.

// ============================================
// BASE CLASS
// ============================================

class User {
  protected readonly id: number;
  public name: string;
  private email: string;

  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  getId(): number {
    return this.id;
  }

  getEmail(): string {
    return this.email;
  }

  greet(): string {
    return `Hello, ${this.name}!`;
  }

  static create(name: string, email: string): User {
    return new User(Math.floor(Math.random() * 1000), name, email);
  }
}

// ============================================
// SUBCLASS
// ============================================

class Admin extends User {
  private permissions: Set<string> = new Set();

  constructor(id: number, name: string, email: string) {
    super(id, name, email);
  }

  grant(permission: string): this {
    this.permissions.add(permission);
    return this;
  }

  can(permission: string): boolean {
    return this.permissions.has(permission);
  }

  override greet(): string {
    return `Admin ${this.name}`;
  }
}

// ============================================
// USAGE
// ============================================

const alice = User.create('Alice', 'alice@example.com');
console.log(alice.greet());
console.log(alice.getId());

const admin = new Admin(2, 'Bob', 'bob@example.com')
  .grant('write')
  .grant('delete');

console.log(admin.greet());        // Admin Bob
console.log(admin.can('write'));   // true
console.log(admin.can('read'));    // false

What this shows:

  • protected readonly id โ€” accessible to subclasses, immutable after construction
  • private email โ€” accessible only inside the class
  • Parameter properties (public name) โ€” declare and assign in one line
  • Static create โ€” factory method
  • override โ€” explicit override of the base method
  • Fluent chain โ€” grant returns this

Why this shape: It’s the essential class toolkit โ€” properties, methods, constructor parameters, access modifiers, inheritance, static methods. Real classes use all of these. The example is small enough to read at once but exercises every concept.


Complete Example Session

# ============================================
# PART 1: BASIC CLASS
# ============================================

cat > basics.ts << 'EOF'
class User {
  id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  greet(): string {
    return `Hello, ${this.name}!`;
  }
}

const alice = new User(1, 'Alice');
console.log(alice.greet());
console.log(alice.id, alice.name);
EOF

npx tsc --noEmit basics.ts
# (no errors)

# ============================================
# PART 2: TRIGGER TYPE ERRORS
# ============================================

cat > errors.ts << 'EOF'
class User {
  id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

const a = new User(1, 'Alice');
// new User('1', 'Alice');        // โŒ wrong arg type
// new User(1);                   // โŒ missing arg
console.log(a.missing);           // โŒ property doesn't exist
EOF

npx tsc --noEmit errors.ts
# [ errors.ts:15:13 - Property 'missing' does not exist on type 'User'. ]

rm errors.ts

# ============================================
# PART 3: ACCESS MODIFIERS
# ============================================

cat > access.ts << 'EOF'
class User {
  public name: string;
  private email: string;
  protected readonly id: number;

  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  getEmail(): string {
    return this.email;
  }
}

class Admin extends User {
  role = 'admin';

  show(): string {
    return `Admin #${this.id}`;  // โœ… protected
  }
}

const a = new User(1, 'Alice', 'a@x.com');
// a.email;  // โŒ private
// a.id;     // โŒ protected
console.log(a.name, a.getEmail());

const admin = new Admin(2, 'Bob', 'b@x.com');
console.log(admin.show());
EOF

npx tsc --noEmit access.ts
# (no errors)

# ============================================
# PART 4: PARAMETER PROPERTIES
# ============================================

cat > params.ts << 'EOF'
class UserService {
  constructor(
    public id: number,
    public name: string,
    private email: string
  ) {}

  getEmail(): string {
    return this.email;
  }
}

const s = new UserService(1, 'Alice', 'a@x.com');
console.log(s.id, s.name, s.getEmail());
EOF

npx tsc --noEmit params.ts
# (no errors)

# ============================================
# PART 5: INHERITANCE
# ============================================

cat > inherit.ts << 'EOF'
class Animal {
  constructor(public name: string) {}

  speak(): string {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);
  }

  override speak(): string {
    return `${this.name} barks`;
  }
}

const d = new Dog('Rex', 'Labrador');
console.log(d.speak(), d.breed);
EOF

npx tsc --noEmit inherit.ts
# (no errors)

# ============================================
# PART 6: STATIC AND PRIVATE CONSTRUCTOR
# ============================================

cat > static.ts << 'EOF'
class Singleton {
  private static instance: Singleton | null = null;
  static count = 0;

  private constructor(public id: number) {
    Singleton.count++;
  }

  static getInstance(): Singleton {
    if (!this.instance) this.instance = new Singleton(1);
    return this.instance;
  }
}

const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b);              // true
console.log(Singleton.count);      // 1
EOF

npx tsc --noEmit static.ts
# (no errors)

# ============================================
# PART 7: COMPILE AND RUN
# ============================================

npx tsc basics.ts access.ts params.ts inherit.ts static.ts
node basics.js
# [ Hello, Alice! ]
# [ 1 Alice ]

node access.js
# [ Alice a@x.com ]
# [ Admin #2 ]

node params.js
# [ 1 Alice a@x.com ]

node inherit.js
# [ Rex barks Labrador ]

node static.js
# [ true ]
# [ 1 ]

Quick Reference

Class Syntax

PartExample
Propertyname: string = ''
Methodgreet(): string { }
Constructorconstructor(id: number) { }
Staticstatic count = 0
Readonlyreadonly id: number
Optionalnickname?: string
Definiteid!: number

Access Modifiers

ModifierAccess
publicAnywhere (default)
privateClass only
protectedClass + subclasses
readonlyAssignable in constructor
#privateRuntime private

Constructor Parameter Properties

SyntaxEffect
constructor(public x: T)Property + assign
constructor(private x: T)Private property + assign
constructor(protected x: T)Protected + assign
constructor(readonly x: T)Readonly + assign
constructor(x: T)Parameter only

Property Initialization

FormMeaning
name = 'x'Inline default
name: stringMust assign in constructor
name?: stringOptional
name!: stringDefinite assignment
readonly name = 'x'Immutable

this Return Type

ReturnPreserves subclass
thisโœ…
ClassNameโŒ loses subclass
voidโŒ no chaining

Static Members

FormAccess
static count = 0Class.count
static create(): TClass.create()
static readonly MAX = 100Class.MAX
static { }Static block

Inheritance

KeywordUse
extendsSubclass
super(...)Call parent constructor
super.method()Call parent method
overrideExplicitly override

Modifiers vs #

private#private
Runtime enforcedโŒโœ…
Bypass via as anyโœ…โŒ
TypeScript supportโœ…โœ…
UseConventionReal privacy

Comparison with Interfaces

AspectClassInterface
Runtimeโœ… existsโŒ erased
Implementationโœ…โŒ
Constructorโœ…โŒ
Stateโœ…โŒ (shape only)
Implementsโœ…N/A

Comparison with Plain Objects

AspectClassPlain object
Methodsโœ…Functions only
Private stateโœ…โŒ
Inheritanceโœ…โŒ (composition)
Serializationโš ๏ธ loses classโœ…
Simple casesOverkillโœ…

Constructor Types

TypeUse
RegularGeneral
Parameter propertiesDI, simple cases
PrivateSingleton, factory-only
ProtectedAbstract-like
OverloadedRare

Common Patterns

PatternExample
DIconstructor(private http: HttpClient)
Factorystatic create(...)
Singletonprivate constructor
Fluentmethod(): this
Abstractabstract class
Immutablereadonly + constructor

Method Overloading

StepCode
Overload 1parse(s: string): object;
Overload 2parse(n: number): number;
Implementationparse(x: string | number) { }

Best Practices

โœ… Do This:

// Type properties explicitly
class User {
  id: number = 0;
  name: string = '';
}                                                        // โœ…

// Initialize all properties
class User {
  id: number;
  constructor(id: number) { this.id = id; }
}                                                        // โœ…

// Use parameter properties for simple cases
class Service {
  constructor(private http: HttpClient) {}
}                                                        // โœ…

// Use readonly for immutable fields
class User {
  readonly id: number;
  constructor(id: number) { this.id = id; }
}                                                        // โœ…

// Use private for internal state
class Counter {
  private count = 0;
  increment(): void { this.count++; }
}                                                        // โœ…

// Use this return for fluent methods
class Builder {
  add(x: number): this { return this; }
}                                                        // โœ…

// Use static factory methods when helpful
class User {
  static create(name: string): User { return new User(name); }
  private constructor(public name: string) {}
}                                                        // โœ…

// Use #private for real encapsulation
class Secret {
  #value = 'x';
  get() { return this.#value; }
}                                                        // โœ…

// Prefer composition over deep inheritance
class Logger { log(msg: string) {} }
class Service {
  constructor(private logger: Logger) {}
}                                                        // โœ…

โŒ Don’t Do This:

// Don't leave properties uninitialized
class User {
  id: number;  // โŒ strict error                        // โŒ
}

// Don't use `!` casually
class User {
  id!: number;  // โš ๏ธ  only if truly assigned elsewhere  // โš ๏ธ
}

// Don't use `any` for properties
class User {
  data: any;                                             // โŒ
}

// Don't mutate readonly after construction
class User {
  readonly id = 1;
  change() { this.id = 2; }  // โŒ                       // โŒ
}

// Don't call `this` before `super` in subclass
class D extends B {
  constructor() {
    this.x = 1;  // โŒ super must be called first         // โŒ
    super();
  }
}

// Don't overuse inheritance
class A extends B extends C extends D { }                // โš ๏ธ  deep hierarchy

// Don't expose internal state directly
class Cache {
  data = {};  // โš ๏ธ  use private                          // โš ๏ธ
}

// Don't use `private` for real secrecy
class Secret {
  private password = 'x';  // โš ๏ธ  bypassable with as any  // โš ๏ธ
}

Common Pitfalls

PitfallProblemSolution
Uninitialized propertyStrict errorInitialize or use !
this before superRuntime errorCall super first
Missing overrideSilent overrideAdd override
private thought as runtimeBypassable via as anyUse # for real privacy
Deep inheritanceFragilePrefer composition
! on unassigned propertyRuntime undefinedInitialize properly
Wrong constructor argsCompile errorMatch the signature
Static state sharedSurprising mutationsBe careful with static
Overriding constructor signatureConfusingFollow Liskov
Type-only importValue needed at runtimeImport the class properly

Real-World Examples

1. Basic class

class User {
  constructor(public name: string) {}
}

2. With methods

class User {
  name = '';
  greet(): string { return `Hi, ${this.name}`; }
}

3. Readonly property

class User {
  readonly id: number;
  constructor(id: number) { this.id = id; }
}

4. Private property

class User {
  private secret = 'x';
  get() { return this.secret; }
}

5. Protected property

class Base {
  protected value = 0;
}
class Child extends Base {
  show() { return this.value; }
}

6. Parameter properties

class Service {
  constructor(private http: HttpClient, public name: string) {}
}

7. Static factory

class User {
  static create(name: string): User { return new User(name); }
  private constructor(public name: string) {}
}

8. Static property

class Counter {
  static count = 0;
}

9. Fluent chain

class Builder {
  values: number[] = [];
  add(n: number): this { this.values.push(n); return this; }
}

10. Inheritance

class Animal {
  constructor(public name: string) {}
}
class Dog extends Animal {
  constructor(name: string, public breed: string) { super(name); }
}

11. Override method

class A { speak(): string { return 'A'; } }
class B extends A {
  override speak(): string { return 'B'; }
}

12. Abstract class

abstract class Shape {
  abstract area(): number;
}
class Circle extends Shape {
  constructor(private r: number) { super(); }
  area() { return Math.PI * this.r ** 2; }
}

13. Optional constructor param

class User {
  constructor(public id: number, public nickname?: string) {}
}

14. Default constructor param

class Config {
  constructor(public host = 'localhost', public port = 8080) {}
}

15. Private constructor (singleton)

class Singleton {
  private static instance: Singleton;
  private constructor() {}
  static get(): Singleton {
    return this.instance ??= new Singleton();
  }
}

16. #private field

class Secret {
  #value = 'x';
  get() { return this.#value; }
}

17. Method overloading

class Parser {
  parse(s: string): object;
  parse(n: number): number;
  parse(x: string | number) { return typeof x === 'string' ? {} : x; }
}

18. Implement interface

interface Comparable<T> {
  compareTo(other: T): number;
}
class Version implements Comparable<Version> {
  constructor(public v: string) {}
  compareTo(other: Version) { return this.v.localeCompare(other.v); }
}

19. Dependency injection

class UserService {
  constructor(private http: HttpClient, private logger: Logger) {}
}

20. Getters and setters

class Temp {
  private _c = 0;
  get celsius() { return this._c; }
  set celsius(v: number) { this._c = v; }
  get fahrenheit() { return this._c * 9/5 + 32; }
}

Visual: Class Anatomy

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class User {                                โ”‚
โ”‚                                              โ”‚
โ”‚    // properties                             โ”‚
โ”‚    id: number                                โ”‚
โ”‚    name: string = ''                         โ”‚
โ”‚    readonly createdAt: Date                  โ”‚
โ”‚    private secret: string                    โ”‚
โ”‚                                              โ”‚
โ”‚    // static                                 โ”‚
โ”‚    static count = 0                          โ”‚
โ”‚                                              โ”‚
โ”‚    // constructor                            โ”‚
โ”‚    constructor(id: number, name: string) {   โ”‚
โ”‚      this.id = id                            โ”‚
โ”‚      this.name = name                        โ”‚
โ”‚    }                                         โ”‚
โ”‚                                              โ”‚
โ”‚    // methods                                โ”‚
โ”‚    greet(): string { }                       โ”‚
โ”‚    private validate(): boolean { }           โ”‚
โ”‚    static create(name: string): User { }     โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Access Modifiers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  public                                      โ”‚
โ”‚  โ”€ anywhere                                  โ”‚
โ”‚  โ”€ default                                   โ”‚
โ”‚                                              โ”‚
โ”‚  protected                                   โ”‚
โ”‚  โ”€ class + subclasses                        โ”‚
โ”‚                                              โ”‚
โ”‚  private                                     โ”‚
โ”‚  โ”€ class only                                โ”‚
โ”‚                                              โ”‚
โ”‚  #private                                    โ”‚
โ”‚  โ”€ class only, runtime enforced              โ”‚
โ”‚                                              โ”‚
โ”‚  readonly                                    โ”‚
โ”‚  โ”€ constructor only                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Outside  Subclass  Same class               โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€              โ”‚
โ”‚  public   โœ…       โœ…        โœ…              โ”‚
โ”‚  protectedโŒ       โœ…        โœ…              โ”‚
โ”‚  private  โŒ       โŒ        โœ…              โ”‚
โ”‚  #private โŒ       โŒ        โœ…              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Parameter Properties

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Longhand                                    โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    name: string;                             โ”‚
โ”‚    email: string;                            โ”‚
โ”‚                                              โ”‚
โ”‚    constructor(name: string, email: string) {โ”‚
โ”‚      this.name = name;                       โ”‚
โ”‚      this.email = email;                     โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Shorthand                                   โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    constructor(                              โ”‚
โ”‚      public name: string,                    โ”‚
โ”‚      public email: string                    โ”‚
โ”‚    ) {}                                      โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Same result โ€” less code                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Inheritance Chain

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Animal {                              โ”‚
โ”‚    constructor(public name: string) {}       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ–ฒ
                  โ”‚ extends
                  โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Dog extends Animal {                  โ”‚
โ”‚    constructor(name: string, breed: string) {โ”‚
โ”‚      super(name);   // โ† must call first     โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Rules:                                      โ”‚
โ”‚  โ€ข super() must be called before this access โ”‚
โ”‚  โ€ข only one parent class                     โ”‚
โ”‚  โ€ข methods can be overridden                 โ”‚
โ”‚  โ€ข use `override` keyword explicitly         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Static vs Instance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Static โ€” on the class                       โ”‚
โ”‚                                              โ”‚
โ”‚  User.count      โ†’ class property            โ”‚
โ”‚  User.create()   โ†’ class method              โ”‚
โ”‚                                              โ”‚
โ”‚  Shared across all instances                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Instance โ€” on the object                    โ”‚
โ”‚                                              โ”‚
โ”‚  user.name       โ†’ instance property         โ”‚
โ”‚  user.greet()    โ†’ instance method           โ”‚
โ”‚                                              โ”‚
โ”‚  Each instance has its own                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: DI via Constructor

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class UserService {                         โ”‚
โ”‚    constructor(                              โ”‚
โ”‚      private http: HttpClient,               โ”‚
โ”‚      private logger: Logger                  โ”‚
โ”‚    ) {}                                      โ”‚
โ”‚                                              โ”‚
โ”‚    async getUser(id: number) {               โ”‚
โ”‚      this.logger.info('fetching');           โ”‚
โ”‚      return this.http.get(`/users/${id}`);   โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  caller provides
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  const http = new HttpClient();              โ”‚
โ”‚  const logger = new Logger();                โ”‚
โ”‚  const service = new UserService(http, logger)โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ easy to swap for tests                    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: this Return Type

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Builder {                             โ”‚
โ”‚    add(x: number): this { return this; }     โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  class FluentBuilder extends Builder {       โ”‚
โ”‚    name(s: string): this { return this; }    โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  new FluentBuilder()                         โ”‚
โ”‚    .add(1)                                   โ”‚
โ”‚    .name('x')   โ† works โ€” `this` preserved   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  If return type was `Builder`:               โ”‚
โ”‚                                              โ”‚
โ”‚  new FluentBuilder()                         โ”‚
โ”‚    .add(1)                                   โ”‚
โ”‚    .name('x')   โŒ name not on Builder       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Class vs Interface

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface โ€” shape only                      โ”‚
โ”‚                                              โ”‚
โ”‚  interface User {                            โ”‚
โ”‚    id: number;                               โ”‚
โ”‚    name: string;                             โ”‚
โ”‚    greet(): string;                          โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข exists at type level                      โ”‚
โ”‚  โ€ข no implementation                         โ”‚
โ”‚  โ€ข erased at runtime                         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class โ€” shape + implementation              โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    id = 0;                                   โ”‚
โ”‚    name = '';                                โ”‚
โ”‚    greet() { return `Hi ${this.name}`; }     โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข exists at runtime                         โ”‚
โ”‚  โ€ข has implementation                        โ”‚
โ”‚  โ€ข has constructor, static, private          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: private vs #private

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  TypeScript `private`                        โ”‚
โ”‚                                              โ”‚
โ”‚  class A { private x = 1; }                  โ”‚
โ”‚                                              โ”‚
โ”‚  const a = new A();                          โ”‚
โ”‚  a.x;              โŒ compile error           โ”‚
โ”‚  (a as any).x;     โœ… bypasses check          โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ compile-time only                         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  JavaScript `#private`                       โ”‚
โ”‚                                              โ”‚
โ”‚  class A { #x = 1; }                         โ”‚
โ”‚                                              โ”‚
โ”‚  const a = new A();                          โ”‚
โ”‚  a.#x;             โŒ syntax error            โ”‚
โ”‚  (a as any).#x;    โŒ still fails             โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ runtime enforced                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Class Design Checklist

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Before writing a class:                     โ”‚
โ”‚                                              โ”‚
โ”‚  [ ] Needs state + behavior?                 โ”‚
โ”‚  [ ] Needs private state?                    โ”‚
โ”‚  [ ] Needs inheritance?                      โ”‚
โ”‚  [ ] Will be serialized?                     โ”‚
โ”‚  [ ] Needs DI?                               โ”‚
โ”‚  [ ] Better as a plain object?               โ”‚
โ”‚                                              โ”‚
โ”‚  If "better as plain object" โ†’ skip class.   โ”‚
โ”‚  Otherwise โ†’ class with typed properties.    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
ClassBlueprint for objects with state and behavior
PropertyTyped field on instances
MethodTyped function on instances
ConstructorInitializes instances
readonlyAssignable only in constructor
staticOn the class, not instances
publicAccessible anywhere (default)
privateClass only (compile-time)
protectedClass and subclasses
#privateRuntime-enforced private
Parameter propertyConstructor shorthand for property + assign
this returnPreserves subclass in chains
overrideExplicit override of base method
superCall parent constructor or method

Key takeaways:

  • Classes bundle state and behavior โ€” properties, methods, and a constructor
  • Every property and method can be typed โ€” the compiler checks them
  • Under strictPropertyInitialization, every property must be initialized at declaration or in the constructor
  • Parameter properties โ€” constructor(private http: HttpClient) โ€” declare and assign in one line
  • readonly allows assignment only in the constructor; private/protected control access
  • #private is real runtime privacy; TypeScript’s private is compile-time only
  • static members live on the class, not instances โ€” factory methods and shared state
  • super() must be called before this in a subclass
  • override makes overriding explicit โ€” catches typos
  • this return type preserves the subclass through fluent chains
  • Constructor injection is the standard DI pattern โ€” testable and explicit
  • Prefer composition over deep inheritance โ€” pass dependencies instead of building tall hierarchies

Remember: Classes in TypeScript are JavaScript classes with a type layer. Type your properties, methods, and constructor parameters โ€” the compiler catches wrong initialization, wrong arguments, and wrong usage. Use access modifiers to control who can do what, and parameter properties for concise DI. Reach for a class when you need state with behavior, private state, or inheritance. Otherwise, a plain object and functions will do.


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!