Published on

A Comprehensive Guide to Interfaces and Classes

TypeScript is a popular language that builds on top of JavaScript by adding static type definitions and other features that make it easier to write and maintain large scale applications. In this blog post, we will delve into two important concepts in TypeScript - interfaces and classes - and see how they can help you write cleaner, more scalable code.

What are Interfaces in TypeScript?

An interface in TypeScript is a contract that defines the shape of an object. It specifies the properties and methods that an object should have, and ensures that objects that implement the interface adhere to this contract.

Here's an example of an interface that defines the shape of a simple object that has two properties - id and name - and a method called getName():

interface MyObject {
  id: number;
  name: string;
  getName(): string;
}

We can then create an object that implements this interface by defining the required properties and methods:

const myObject: MyObject = {
  id: 1,
  name: 'My Object',
  getName() {
    return this.name;
  }
};

Interfaces are purely for type checking and are not included in the generated JavaScript code. This means that you can use them to enforce a certain structure in your code without incurring any runtime overhead.

Why Use Interfaces in TypeScript?

Interfaces are a powerful tool in TypeScript because they allow you to define a contract that other objects must follow. This can be useful in a number of situations, such as:

  1. Ensuring that objects have a certain structure: By defining an interface for an object, you can ensure that all objects that implement the interface have the required properties and methods. This can help you catch errors early on and avoid runtime bugs.

  2. Documenting code: Interfaces can serve as a clear and concise way of documenting the structure of an object. This can make it easier for other developers to understand your code and work with it.

  3. Enforcing consistency: If you have a large codebase with many objects, using interfaces can help you ensure that all objects have a consistent structure. This can make it easier to work with the objects and reduce the risk of bugs.

Advanced Features of Interfaces in TypeScript

TypeScript's interfaces are quite powerful and offer a number of advanced features that can help you write more robust code. Some of these features include:

  1. Optional properties: You can mark a property as optional by adding a ? at the end of its name. This allows objects that implement the interface to omit the property if needed.
interface MyObject {
  id: number;
  name?: string; // Optional property
  getName(): string;
}
  1. Readonly properties: You can mark a property as readonly by adding the readonly keyword. This prevents the property from being modified after the object is created.
interface MyObject {
  readonly id: number; // Readonly property
  name: string;
  getName(): string;
}
  1. Function types: You can specify that a property should be a function by using the => notation. This can be useful for defining callback functions or other functions that need to be passed as arguments.
interface MyObject {
  id: number;
  name: string;
  getName(): string;
  onChange: (newName: string) => void; // Function type
}
  1. Indexable types: You can use an index signature to define an object that can be indexed by a specific type, such as a string or a number. This can be useful for defining objects that behave like dictionaries or maps.
interface MyObject {
  id: number;
  name: string;
  getName(): string;
  [key: string]: any; // Indexable type
}
  1. Extending interfaces: You can use the extends keyword to create an interface that extends another interface. This allows you to create a new interface that includes all the properties and methods of the parent interface, and add additional ones as needed.
interface MyObject {
  id: number;
  name: string;
  getName(): string;
}

interface MyExtendedObject extends MyObject {
  description: string;
  getDescription(): string;
}

What are Classes in TypeScript?

A class in TypeScript is a blueprint for creating objects. It defines the properties and methods that the objects will have, and provides a way to create and initialize them.

Here's an example of a simple class that has a single property - name - and a method called getName():

class MyClass {
  name: string;

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

  getName() {
    return this.name;
  }
}

We can create an instance of this class using the new keyword:

const myClass = new MyClass('My Class');
console.log(myClass.getName()); // Outputs: "My Class"

Classes in TypeScript are compiled to JavaScript classes, which means that they are implemented using the prototype system. This allows you to use inheritance and other object-oriented concepts in your code.

Why Use Classes in TypeScript?

Classes are a useful tool in TypeScript because they provide a way to organize and structure your code in a logical and reusable way. Some of the benefits of using classes include:

  1. Encapsulation: Classes allow you to encapsulate data and behavior within a single unit. This can make it easier to reason about your code and reduce the risk of bugs.

  2. Inheritance: Classes support inheritance, which allows you to create a new class that extends an existing one. This can be useful for creating a hierarchy of classes and reusing common code.

  3. Polymorphism: Classes support polymorphism, which allows you to define multiple implementations of the same method or property. This can be useful for creating flexible and reusable code.

Advanced Features of Classes in TypeScript

TypeScript's classes are quite powerful and offer a number of advanced features that can help you write more sophisticated code. Some of these features include:

  1. Access modifiers: TypeScript supports three access modifiers - public, private, and protected - which allow you to control the visibility of class properties and methods.

    • public: Properties and methods marked as public are accessible from anywhere within the class and its instances. This is the default access level.

    • private: Properties and methods marked as private are only accessible within the class itself. Attempting to access them from outside the class will result in an error.

    • protected: Properties and methods marked as protected are only accessible within the class and its subclasses. This can be useful for creating a base class that can be extended but has certain properties and methods that should not be accessed directly.

    class MyClass {
      private secret: string; // Private property
      protected secret2: string; // Protected property
      public name: string; // Public property
    
      constructor(name: string) {
        this.name = name;
      }
    
      private getSecret() { // Private method
        return this.secret;
      }
    
      protected getSecret2() { // Protected method
        return this.secret2;
      }
    
      public getName() { // Public method
        return this.name;
      }
    }
    
  2. Static properties and methods: You can use the static keyword to define properties and methods that are shared across all instances of a class. These properties and methods can be accessed directly from the class itself, without needing to create an instance.

class MyClass {
  static count: number = 0; // Static property
  name: string;

  constructor(name: string) {
    this.name = name;
    MyClass.count++;
  }

  static getCount() { // Static method
    return MyClass.count;
  }
}

console.log(MyClass.getCount()); // Outputs: 0
const myClass1 = new MyClass('My Class 1');
console.log(MyClass.getCount()); // Outputs: 1
const myClass2 = new MyClass('My Class 2');
console.log(MyClass.getCount()); // Outputs: 2
  1. Abstract classes: You can use the abstract keyword to define an abstract class, which is a class that cannot be instantiated but can be extended by other classes. Abstract classes are often used as base classes that provide common functionality but leave the implementation of certain methods to the subclasses.
abstract class MyAbstractClass {
  abstract getValue(): number;
}

class MyClass extends MyAbstractClass {
  getValue() {
    return 42;
  }
}

const myClass = new MyClass();
console.log(myClass.getValue()); // Outputs: 42
  1. Getters and setters: You can use the get and set keywords to define getter and setter methods for properties. This can be useful for adding custom logic when a property is accessed or modified.
class MyClass {
  private _name: string; // Private property

  get name() { // Getter method
    return this._name;
  }

  set name(name: string) { // Setter method
    this._name = name.toUpperCase();
  }
}

const myClass = new MyClass();
myClass.name = 'My Class';
console.log(myClass.name); // Outputs: "MY CLASS"
  1. Interfaces and classes: You can use interfaces to define contracts that classes must follow. This can be useful for enforcing a certain structure in your classes and ensuring that they have the required properties and methods.
interface MyObject {
  id: number;
  name: string;
  getName(): string;
}

class MyClass implements MyObject {
  id: number;
  name: string;

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

  getName() {
    return this.name;
  }
}

const myClass = new MyClass(1, 'My Class');
console.log(myClass.getName()); // Outputs: "My Class"
  1. Constructor functions: In TypeScript, you can use a special kind of function called a "constructor function" to create and initialize an object. This can be useful for creating objects with a certain structure and default values.
function MyConstructorFunction(id: number, name: string) {
  return {
    id,
    name,
    getName() {
      return this.name;
    }
  };
}

const myObject = MyConstructorFunction(1, 'My Object');
console.log(myObject.getName()); // Outputs: "My Object"
  1. Union types and interfaces: You can use union types in combination with interfaces to create more flexible type definitions. This can be useful for defining objects that can have multiple shapes.
interface MyObject {
  id: number;
}

interface MyObjectWithName extends MyObject {
  name: string;
  getName(): string;
}

type MyObjectUnion = MyObject | MyObjectWithName;

function logName(obj: MyObjectUnion) {
  if ('name' in obj) {
    console.log(obj.getName());
  } else {
    console.log('Object has no name');
  }
}

const myObject1: MyObject = { id: 1 };
logName(myObject1); // Outputs: "Object has no name"

const myObject2: MyObjectWithName = { id: 2, name: 'My Object', getName() { return this.name; } };
logName(myObject2); // Outputs: "My Object"

Conclusion

Interfaces and classes are two important concepts in TypeScript that can help you write cleaner, more scalable code. By using interfaces to define contracts and classes to implement them, you can take advantage of the benefits of static typing and object-oriented programming in your code.

I hope this comprehensive guide to interfaces and classes in TypeScript has helped you understand these concepts and how to use them effectively. If you have any questions or want to learn more about TypeScript, feel free to leave a comment below.

Loading comments...