5. Interfaces & Objects

In TypeScript, interfaces play a crucial role in defining the structure and shape of objects, enabling developers to write type-safe and well-structured code. In this article, we will introduce you to interfaces and explain how to create them.

Introduction to Interfaces

An interface in TypeScript is a way to define a contract or a blueprint for objects. It specifies the properties (names and data types) and methods (function signatures) that an object should have. Interfaces provide a clear way to define the structure of objects and enforce type-checking.

Consider a scenario where you’re working with different shapes in a geometry application. You might have objects representing circles, rectangles, and triangles, all of which have common properties like area and perimeter. To ensure that these objects adhere to a specific structure, you can define an interface.

Creating Interfaces

To create an interface in TypeScript, use the interface keyword followed by the interface’s name. Inside the interface, define the properties and methods that should be present in objects adhering to the interface.

interface Shape {
    area(): number;
    perimeter(): number;
}

// Implementing the Shape interface for a Circle
class Circle implements Shape {
    constructor(private radius: number) {}

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

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

// Implementing the Shape interface for a Rectangle
class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}

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

    perimeter(): number {
        return 2 * (this.width + this.height);
    }
}

In this example, we define an Shape interface with two methods: area and perimeter, both returning numbers. We then create two classes, Circle and Rectangle, that implement the Shape interface. By doing so, these classes must provide implementations for the area and perimeter methods as specified in the interface.

Now, let’s create instances of these classes and use them:

const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);

console.log(`Circle Area: ${circle.area()}`);
console.log(`Circle Perimeter: ${circle.perimeter()}`);
console.log(`Rectangle Area: ${rectangle.area()}`);
console.log(`Rectangle Perimeter: ${rectangle.perimeter()}`);

By adhering to the Shape interface, the Circle and Rectangle objects must provide implementations for the area and perimeter methods. This ensures that the code remains type-safe, and any deviations from the specified structure would result in compile-time errors.

Object Shape and Type Compatibility

TypeScript employs a structural type system, which means that the compatibility of types is determined by their structure or shape rather than their explicitly declared names. If two objects have the same structure (i.e., matching property names and types), they are considered compatible, even if they don’t share a common interface or class.

interface Point {
    x: number;
    y: number;
}

const pointA: Point = { x: 1, y: 2 };
const pointB = { x: 3, y: 4 }; // Infers the Point type

pointA = pointB; // Allowed
pointB = pointA; // Allowed

In this example, pointA and pointB both have the same shape (properties x and y with number types), so they are compatible. You can assign pointB to pointA and vice versa without issues.

Readonly Properties

In some cases, you may want to ensure that object properties remain constant after initialization. TypeScript allows you to mark properties as readonly to prevent their modification after assignment.

interface Circle {
    readonly radius: number;
}

const circle: Circle = { radius: 5 };

// Attempting to modify a readonly property results in an error
circle.radius = 10; // Error: Cannot assign to 'radius' because it is a read-only property.

By marking the radius property as readonly, you ensure that its value cannot be changed once it is set. This is particularly useful when you want to enforce immutability in your objects.

Extending Interfaces

Interfaces in TypeScript can extend other interfaces to create new interfaces that inherit properties and methods from existing ones. This allows you to build on top of existing structures while keeping your code modular and organized.

interface Animal {
    name: string;
    speak(): void;
}

interface Dog extends Animal {
    breed: string;
}

const myDog: Dog = {
    name: "Buddy",
    speak() {
        console.log("Woof!");
    },
    breed: "Golden Retriever",
};

In this example, the Dog interface extends the Animal interface, inheriting its properties and methods. This hierarchical structure enables you to create more specialized interfaces while maintaining code reusability.

Conclusion

Interfaces in TypeScript provide a powerful mechanism for defining the structure of objects, facilitating code organization and type-checking. They are invaluable for creating contracts between different parts of your codebase and ensuring that objects adhere to specific shapes and behaviors. As you delve deeper into TypeScript development, you’ll find interfaces to be a fundamental tool for writing clean and maintainable code.

Understanding object shape and type compatibility, utilizing readonly properties, and extending interfaces are essential aspects of TypeScript that contribute to code robustness, immutability, and organization. These concepts empower you to write safer and more maintainable code, especially when working with complex data structures and object hierarchies.

Previous
Next