JavaScript > TypeScript > TypeScript Basics > Interfaces

Defining Shapes with TypeScript Interfaces

This example demonstrates how to define and implement interfaces in TypeScript to represent shapes with specific properties and methods. Interfaces provide a powerful way to define contracts for classes and objects, ensuring type safety and code maintainability.

Basic Interface Definition

This code defines an interface named Shape with two members: a color property of type string and a getArea method that returns a number. The Circle and Square classes implement this interface, ensuring that they both have a color property and a getArea method. The implements keyword is crucial for enforcing the interface contract.

interface Shape {
  color: string;
  getArea(): number;
}

class Circle implements Shape {
  constructor(public radius: number, public color: string) {}

  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

class Square implements Shape {
  constructor(public sideLength: number, public color: string) {}

  getArea() {
    return this.sideLength ** 2;
  }
}

Using the Interface

This snippet creates instances of Circle and Square and assigns them to variables of type Shape. This demonstrates how interfaces can be used to define a common type for different classes that share a similar structure. The console.log statements output the area of each shape.

const myCircle: Shape = new Circle(5, 'red');
const mySquare: Shape = new Square(10, 'blue');

console.log(`Circle Area: ${myCircle.getArea()}`);
console.log(`Square Area: ${mySquare.getArea()}`);

Optional Properties

The name?: string syntax defines an optional property in the Shape interface. Classes implementing the interface are not required to define this property. This allows for more flexible data structures. The Triangle class implements the Shape interface with the name property being optional.

interface Shape {
  color: string;
  getArea(): number;
  name?: string; // Optional property
}

class Triangle implements Shape {
  color: string;
  base: number;
  height: number;
  name?: string;

  constructor(color: string, base: number, height: number, name?: string) {
    this.color = color;
    this.base = base;
    this.height = height;
    this.name = name;
  }

  getArea(): number {
    return 0.5 * this.base * this.height;
  }
}

Readonly Properties

The readonly id: number syntax defines a property that can only be set during object creation. Any attempt to modify it later will result in a TypeScript error. This is useful for properties that should not be changed after initialization, such as unique identifiers.

interface Shape {
  readonly id: number;
  color: string;
  getArea(): number;
}

class Rectangle implements Shape {
  readonly id: number;
  color: string;
  width: number;
  height: number;

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

  getArea(): number {
    return this.width * this.height;
  }
}

Extending Interfaces

This demonstrates how to extend interfaces using the extends keyword. The Shape interface now inherits the move method from the Movable interface, requiring any class implementing Shape to also implement the move method. The MovableCircle class implements both the Shape and Movable interfaces.

interface Movable {
  move(distance: number): void;
}

interface Shape extends Movable {
  color: string;
  getArea(): number;
}

class MovableCircle implements Shape {
  color: string;
  radius: number;

  constructor(color: string, radius: number) {
    this.color = color;
    this.radius = radius;
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }

  move(distance: number): void {
    console.log(`Moving circle ${distance} units`);
  }
}

Real-Life Use Case

Interfaces are heavily used in frameworks like React and Angular to define the structure of component props and state. They ensure that components receive the expected data types, preventing runtime errors and improving code reliability. Consider a React component that displays user data; an interface can be defined to specify the structure of the user object, ensuring that the component always receives the necessary information.

Best Practices

  • Use descriptive names: Interface names should clearly indicate their purpose (e.g., Validatable instead of just V).
  • Keep interfaces focused: Each interface should represent a single responsibility or concept.
  • Favor composition over inheritance: While interface extension is useful, consider using composition (creating new interfaces from existing ones) when possible for greater flexibility.

Interview Tip

Be prepared to explain the difference between interfaces and type aliases in TypeScript. While both can be used to define type structures, interfaces can be implemented by classes and extended by other interfaces, while type aliases are more flexible for defining union types and other complex type structures.

When to Use Them

Use interfaces when you want to define a contract for classes or objects. They are particularly useful for ensuring that different parts of your codebase adhere to a consistent structure, improving code maintainability and reducing the risk of errors. They are ideal for defining the shape of data that will be passed between functions or components.

Memory Footprint

Interfaces themselves don't contribute to the runtime memory footprint of your JavaScript code. They are purely a TypeScript construct used for type checking during compilation. Once the code is transpiled to JavaScript, the interface definitions are removed.

Alternatives

Alternatives to interfaces include type aliases and abstract classes. Type aliases can define the shape of an object, but unlike interfaces, they cannot be implemented by classes. Abstract classes can provide a base class with some implementation, but they cannot be implemented by multiple classes like interfaces can.

Pros

  • Type safety: Interfaces enforce type checking at compile time, reducing runtime errors.
  • Code maintainability: They provide a clear contract for classes and objects, making it easier to understand and maintain the code.
  • Flexibility: Interfaces can be extended and implemented by multiple classes, providing flexibility in code design.

Cons

  • Compile-time only: Interfaces are removed during compilation, so they don't provide any runtime type checking.
  • Can be verbose: Defining interfaces can add some boilerplate code, especially for complex data structures.

FAQ

  • What is the difference between an interface and a type alias in TypeScript?

    Interfaces define a contract that classes can implement, and they can be extended by other interfaces. Type aliases simply create a new name for an existing type and are more flexible for creating union types or complex types. Interfaces are generally preferred when defining contracts for objects, while type aliases are useful for other type manipulations.
  • Can an interface have optional properties?

    Yes, you can define optional properties in an interface using the ? symbol after the property name (e.g., name?: string). Classes implementing the interface are not required to define the optional properties.
  • Are interfaces present in the compiled JavaScript code?

    No, interfaces are a TypeScript-specific feature and are removed during compilation. They are used for type checking at compile time but do not exist in the generated JavaScript code.