Properties and Methods in TypeScript

In TypeScript, classes serve as blueprints for creating objects. To make these objects useful, we need two things: Properties to store data (the "state") and Methods to define actions (the "behavior"). While JavaScript handles these concepts dynamically, TypeScript adds a layer of static typing. This means you can catch bugs like trying to call a method that doesn't exist or assigning a string to a number before you even run your code.

 

Defining Properties

Properties are variables that live inside a class. In TypeScript, you explicitly declare these properties and their types at the top of the class body. This makes it immediately clear to any developer what data a class is responsible for managing.

class Car {
  make: string;
  model: string;
  year: number;

  constructor(make: string, model: string, year: number) {
    this.make = make;
    this.model = model;
    this.year = year;
  }
}

const myCar = new Car("Toyota", "Corolla", 2020);
console.log(myCar.make);  // Output: Toyota

In this example, make, model, and year are properties. By typing them, we ensure that a Car can never accidentally have a "year" that is a string, which could break date-based logic elsewhere in your app.

Best Practice: Use TypeScript's "Parameter Properties" shorthand to reduce boilerplate. You can define and initialize properties directly in the constructor by adding an access modifier (like public or private) before the parameter name: constructor(public make: string) {}.

Defining Methods

Methods are functions defined inside a class that operate on the object's data. They allow objects to "do" things. In TypeScript, you should always define the types for method parameters and the return value to maintain a strict contract throughout your codebase.

class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  // A method that calculates data based on properties
  calculateArea(): number {
    return this.width * this.height;
  }
}

const rect = new Rectangle(5, 10);
console.log(rect.calculateArea());  // Output: 50

By specifying : number as the return type for calculateArea, TypeScript will alert you if you accidentally try to return a string or forget the return statement entirely.

Developer Tip: Use methods to encapsulate logic. Instead of letting external code calculate the area of your rectangle manually, provide a method. This ensures that if the formula ever changes, you only have to update it in one place.

Read-Only Properties

Sometimes you want a property to be set once (usually during initialization) and never changed again. This is common for unique IDs, configuration settings, or hardware specifications. Use the readonly keyword to enforce this immutability.

class Product {
  readonly id: string;
  name: string;

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

const product = new Product("8f92-b21a", "Laptop");
console.log(product.id);  // Output: 8f92-b21a
// product.id = "new-id";  // Error: Cannot assign to 'id' because it is a read-only property.
Watch Out: The readonly check only happens at compile-time. If you are working in a mixed environment or using any, the underlying JavaScript will still allow the value to be changed at runtime unless you use other JS features like Object.freeze().

Optional Properties

In real-world applications, not all data is available upfront. TypeScript allows you to mark properties as optional using the ? symbol. This tells the compiler that the property might be a specific type, or it might be undefined.

class Employee {
  id: number;
  name: string;
  department?: string; // This property is optional

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

const emp1 = new Employee(1, "John");
const emp2 = new Employee(2, "Jane", "Engineering");

console.log(emp1.department);  // Output: undefined
Common Mistake: Forgetting to check if an optional property exists before using it. If you try to call emp1.department.toLowerCase(), your code will crash because department is undefined. Always use optional chaining (emp1.department?.toLowerCase()).

Method Overloading

TypeScript supports method overloading, which allows a single method to handle different types or numbers of arguments. Note that in TypeScript, you provide multiple "signatures" but only one actual implementation that must handle all cases.

class Logger {
  // Overload signatures
  log(message: string): void;
  log(message: string, code: number): void;

  // Single implementation signature
  log(message: string, code?: number): void {
    if (code) {
      console.log(`[Error ${code}]: ${message}`);
    } else {
      console.log(`[Info]: ${message}`);
    }
  }
}

const myLogger = new Logger();
myLogger.log("System started");          // Output: [Info]: System started
myLogger.log("Connection failed", 500);  // Output: [Error 500]: Connection failed

This is extremely useful when building APIs or libraries where you want to provide a flexible interface for other developers without writing multiple method names like logWithMessage and logWithMessageAndCode.

Static Properties and Methods

Static members belong to the class itself, not to any specific instance (object) of the class. They are useful for utility functions or global constants that relate to the class but don't require data from a specific instance.

class AppConfig {
  static apiEndpoint: string = "https://api.example.com/v1";

  static getFullUrl(resource: string): string {
    return `${this.apiEndpoint}/${resource}`;
  }
}

// Access without creating an instance
console.log(AppConfig.apiEndpoint); 
console.log(AppConfig.getFullUrl("users"));
Developer Tip: Use static methods for "Factory" patterns methods that handle complex logic for creating and returning new instances of the class.

Access Modifiers

Access modifiers control who can see and modify the members of your class. This is the cornerstone of Encapsulation, a principle that keeps your internal logic hidden and safe from outside interference.

  • public: The default. Anyone can access it.
  • private: Only code inside the class can see it. Use this for internal state.
  • protected: Only the class and its subclasses (children) can see it.
class Person {
  public name: string;
  private socialSecurityNumber: string;

  constructor(name: string, ssn: string) {
    this.name = name;
    this.socialSecurityNumber = ssn;
  }

  public getMaskedSSN(): string {
    return `***-**-${this.socialSecurityNumber.slice(-4)}`;
  }
}

const person = new Person("Alice", "123-45-6789");
console.log(person.name);          // Alice (Accessible)
console.log(person.getMaskedSSN()); // ***-**-6789 (Accessible)
// console.log(person.socialSecurityNumber); // Error: Private property!
Best Practice: Follow the "Principle of Least Privilege." Start by making all properties private or protected. Only make them public if you are absolutely sure they need to be accessed from outside the class.

 

Summary

Properties and methods are the building blocks of object-oriented programming in TypeScript. By leveraging types, access modifiers, and read-only constraints, you can create code that is self-documenting and much harder to break. Understanding how to hide sensitive data with private and provide flexible interfaces with overloading is what separates a beginner from a professional TypeScript developer.