Skip to main content

TS Ep 16: Class Basics, Field Declarations, and Constructors

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 16: This Article
JavaScript introduced native classes in ES6 (ES2015). TypeScript builds on ES6 classes by adding strong static typing for field declarations, constructor parameter validation, readonly modifiers, and strict initialization rules.

1. Class Field Declarations in TypeScript
#

In plain JavaScript, you can dynamically assign properties to this inside a constructor without declaring them anywhere else.

In TypeScript, all instance properties must be declared in the class body before they can be assigned or accessed.

class DatabaseClient {
  // 1. Explicit Field Declarations
  host: string;
  port: number;
  readonly isCluster: boolean; // Readonly instance field!

  // 2. Constructor
  constructor(host: string, port: number, isCluster = false) {
    this.host = host;
    this.port = port;
    this.isCluster = isCluster;
  }

  // 3. Instance Method
  getConnectionUri(): string {
    return `postgres://${this.host}:${this.port}`;
  }
}

const client = new DatabaseClient("localhost", 5432);
console.log(client.getConnectionUri()); // "postgres://localhost:5432"

// ❌ Compiler Error: Cannot assign to 'isCluster' because it is a read-only property.
// client.isCluster = true;

2. strictPropertyInitialization Mechanics
#

When "strict": true (or "strictPropertyInitialization": true) is enabled in tsconfig.json, TypeScript verifies that every declared class field is initialized either:

  • Directly at its declaration site (inline initializer), OR
  • Inside the constructor() body.

If a field is declared but not initialized, TypeScript throws a compile-time error:

class ApiService {
  // ❌ Compiler Error: Property 'endpoint' has no initializer 
  // and is not definitely assigned in the constructor.
  endpoint: string; 

  constructor() {
    // Oops! Forgot to assign this.endpoint = ...
  }
}

Why this rule exists:
#

In JavaScript, an uninitialized property defaults to undefined at runtime. Calling .toUpperCase() on this.endpoint would crash the application with an unhandled TypeError.


3. 4 Strategies for Uninitialized Fields
#

How do you handle class fields that cannot be assigned immediately inside the constructor?

Strategy 1: Inline Default Initializer (Preferred)
#

Provide a default value right at the field declaration site:

class HTTPConfig {
  timeoutMs: number = 5000; // Inferred as 'number', initialized automatically
  headers: Record<string, string> = {};
}

Strategy 2: Union with null or undefined
#

If the field is intentionally empty upon instantiation:

class UserSession {
  token: string | null = null; // Explicitly nullable
  lastLogin: Date | undefined;

  setToken(newToken: string) {
    this.token = newToken;
  }
}

Strategy 3: Optional Property Modifier (?)
#

Marks the field as optional (type becomes T | undefined):

class Order {
  id: string;
  couponCode?: string; // Type is string | undefined

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

Strategy 4: Definite Assignment Assertion Operator (!)
#

If a property is initialized indirectly by a framework helper method (such as Angular @Input(), TypeORM @Column(), or a custom .init() setup function), you can append ! to tell TypeScript: “Trust me, this field will be assigned before it is ever read.”

class OrmUserEntity {
  // The '!' suppresses strictPropertyInitialization check
  id!: string; 
  email!: string;

  // Framework calls this lifecycle hook after constructor execution
  __initializeFromDatabase(row: { id: string; email: string }) {
    this.id = row.id;
    this.email = row.email;
  }
}
Caution

The Danger of !: The Definite Assignment Operator completely bypasses TypeScript’s safety net. If code reads entity.id before __initializeFromDatabase() runs, your app will crash at runtime with an unhandled TypeError.


4. Class Methods and this Binding
#

Class methods in TypeScript are typed just like standard functions:

class Calculator {
  value: number = 0;

  add(amount: number): this {
    this.value += amount;
    return this; // Polymorphic 'this' for chaining!
  }

  // Arrow function property preserves 'this' context automatically!
  reset = (): void => {
    this.value = 0;
  };
}

const calc = new Calculator();
calc.add(10).add(20);
console.log(calc.value); // 30

Summary & Next Steps
#

In this episode:

  • We learned that TypeScript requires declaring all instance fields in the class body.
  • We analyzed strictPropertyInitialization and why uninitialized fields throw errors.
  • We explored the 4 strategies for handling uninitialized properties (Default Initializers, Unions, Optional ?, and Definite Assertions !).
  • We used readonly class fields for immutable property protection.

In Episode 17: Access Modifiers (public, private, protected), we will explore how to restrict class member visibility, and compare TypeScript’s compile-time modifiers with JavaScript’s native # private fields!

typescript - This article is part of a series.
Part 16: This Article