Structural typing in Typescript


TypeScript, a superset of JavaScript, introduces static typing to JavaScript, allowing developers to catch potential errors and enhance code quality. One of the key features that set TypeScript apart is its support for structural typing. While other statically typed languages often rely on nominal typing, TypeScript embraces the concept of structural typing, which provides a more flexible and intuitive approach to type checking.

In this tutorial, we will explore the concept of structural typing in TypeScript and its benefits and provide relevant examples to illustrate its usage.

Understanding Structural Typing

Structural typing is a type system that focuses on the shape or structure of an object rather than its specific name or class. In other words, two objects are considered compatible if they have the same set of properties and methods, regardless of their explicit declaration or inheritance hierarchy. This approach promotes code reusability and encourages the use of duck typing, where the suitability of an object is determined by its behavior rather than its class or type.

What is Duck Typing?

Duck typing is a concept in programming that focuses on an object's behavior rather than its specific type or class. The term "duck typing" originates from the phrase, "If it looks like a duck and quacks like a duck, then it's probably a duck." In other words, duck typing determines the suitability of an object based on whether it supports the required methods and properties rather than relying on explicit type declarations.

Benefits of Structural Typing

Example 1

Duck Typing and Polymorphism − Structural typing enables duck typing, allowing developers to write more flexible and reusable code. Polymorphism, a fundamental principle in object-oriented programming, becomes easier to achieve since objects with matching structures can be used interchangeably.

In the below example, the printItem function accepts an object of type Printable. Any object that has a print method can be passed to this function, regardless of its explicit declaration or class.

interface Printable {
   print(): void;
}

class Doc implements Printable {
   print() {
      console.log("Printing document...");
   }
}

class DocExtended implements Printable {
   print(): void {
      console.log("Printing from extended documented...");
   }
   wow(): void {
      console.log("This works!!");
   }
}

function printItem(item: Printable) {
   item.print();
}

const doc = new Doc();
printItem(doc);
const docExtended = new DocExtended();
printItem(docExtended);

On compiling, it will generate the following JavaScript code −

var Doc = /** @class */ (function () {
   function Doc() {
   }
   Doc.prototype.print = function () {
      console.log("Printing document...");
   };
   return Doc;
}());
var DocExtended = /** @class */ (function () {
   function DocExtended() {
   }
   DocExtended.prototype.print = function () {
      console.log("Printing from extended documented...");
   };
   DocExtended.prototype.wow = function () {
      console.log("This works!!");
   };
   return DocExtended;
}());
function printItem(item) {
   item.print();
}
var doc = new Doc();
printItem(doc);
var docExtended = new DocExtended();
printItem(docExtended);

Output

The above code will produce the following output −

Printing document...
Printing from extended documented...

Example 2

Structural Subtyping − Structural typing enables implicit interfaces, also known as structural subtyping. Instead of explicitly defining interfaces, TypeScript allows objects to implicitly conform to an expected structure. This simplifies code maintenance by reducing the need for explicit interface declarations.

In the below example, the logMessage function expects an object with a text property of type string. TypeScript infers the type based on the object's structure, allowing us to pass an object literal directly without defining its type.

function logMessage(message: { text: string }) {
   console.log(message.text);
}

const message = { text: "Hello, world!" };
logMessage(message);

On compiling, it will generate the following JavaScript code −

function logMessage(message) {
   console.log(message.text);
}
var message = { text: "Hello, world!" };
logMessage(message);

Output

The above code will produce the following output −

Hello, world!

Example 3

Flexible Type Compatibility − Structural typing provides greater flexibility when it comes to type compatibility. Two objects that have the same structure can be assigned to each other, even if their explicit types are different. This facilitates interoperability and code reuse across different parts of an application.

In the above example, a Circle object is assigned to a variable of type Shape since the structure of the Circle matches the properties defined in the Shape interface. The extra methods like sayHello or properties defined inside the Circle class are not accessible once it is defined of type Shape. Hence sayHello function can’t be accessed on the shape object.

interface Shape {
   color: string;
   display: () =&g; void;
}

class Circle {
   color: string;
   radius: number;

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

   display(): void {
      console.log(`The value of color is: ${this.color}`);
      console.log(`The value of radius is: ${this.radius}`);
   }
   sayHello() {
      console.log(
         "Hey there! I am a circle but still compatible with Shape interface..."
      );
   }
}
const shape: Shape = new Circle("red", 5);
shape.display();

On compiling, it will generate the following JavaScript code −

var Circle = /** @class */ (function () {
   function Circle(color, radius) {
      this.color = color;
      this.radius = radius;
   }
   Circle.prototype.display = function () {
      console.log("The value of color is: ".concat(this.color));
      console.log("The value of radius is: ".concat(this.radius));
   };
   Circle.prototype.sayHello = function () {
      console.log("Hey there! I am a circle but still compatible with Shape interface...");
   };
   return Circle;
}());
var shape = new Circle("red", 5);
shape.display();

Output

The above code will produce the following output −

The value of color is: red
The value of radius is: 5

Example 4

Open Extensible Systems − Structural typing allows for the creation of open and extensible systems, where new types can be added and integrated seamlessly. Since compatibility is based on the structure of objects, adding new properties or methods to an existing object does not break compatibility with other parts of the codebase. This makes it easier to evolve and extend code without causing cascading changes throughout the system.

In this example, even though the circle object has an additional radius property, it is still compatible with the Shape interface as long as it has a color property.

interface Shape {
   color: string;
}

function printShapeColor(shape: Shape) {
   console.log(shape.color);
}
const circle = { color: "blue", radius: 5 };
printShapeColor(circle); // Prints "blue"

On compiling, it will generate the following JavaScript code −

function printShapeColor(shape) {
   console.log(shape.color);
}
var circle = { color: "blue", radius: 5 };
printShapeColor(circle); // Prints "blue"

Output

The above code will produce the following output −

blue

Example 5

Implicit Conversion and Interoperability − Structural typing enables implicit conversions between types that have compatible structures. This makes it easier to work with libraries or code from external sources that might not explicitly match the expected types. TypeScript can automatically infer the structural compatibility and perform the necessary conversions without requiring explicit type annotations or conversions.

In this example, the customer object has an additional address property, but TypeScript can still infer its compatibility with the Person interface, allowing it to be passed to the greet function without errors.

interface Person {
   name: string;
   age: number;
}

function greet(person: Person) {
   console.log(`Hello, ${person.name}! You are ${person.age} years old.`);
}
const customer = { name: "Alice", age: 30, address: "123 Street" };
greet(customer); // Prints "Hello, Alice! You are 30 years old."

On compiling, it will generate the following JavaScript code −

function greet(person) {
   console.log("Hello, ".concat(person.name, "! You are ").concat(person.age, " years old."));
}
var customer = { name: "Alice", age: 30, address: "123 Street" };
greet(customer); // Prints "Hello, Alice! You are 30 years old."

Output

The above code will produce the following output −

Hello, Alice! You are 30 years old.

Example 6

Easy Integration with Existing JavaScript Code − TypeScript's structural typing allows for easy integration with existing JavaScript codebases. Since JavaScript is dynamically typed and often relies on duck typing, structural typing aligns well with JavaScript's runtime behavior. Developers can gradually introduce TypeScript to their JavaScript projects without the need to immediately define explicit interfaces for all objects.

In this JavaScript code snippet, there is no explicit typing or interface definition. TypeScript can infer the structure of the greeting object and ensure type safety when interacting with the printMessage function.

// JavaScript code
function printMessage(message) {
   console.log(message);
}

const greeting = { text: "Hello, world!" };
printMessage(greeting); // Prints "{ text: "Hello, world!" }"

On compiling, it will generate the following JavaScript code −

// JavaScript code
function printMessage(message) {
   console.log(message);
}
var greeting = { text: "Hello, world!" };
printMessage(greeting); // Prints "{ text: "Hello, world!" }"

Output

The above code will produce the following output −

{ text: 'Hello, world!' }

Conclusion

Structural typing in TypeScript is a powerful feature that promotes flexibility, code reuse, and interoperability. By focusing on the structure of objects rather than their explicit types, TypeScript enables developers to write more expressive and adaptable code. The ability to leverage duck typing, achieve polymorphism, and enjoy flexible type compatibility provides significant advantages in terms of code maintainability and scalability. As you continue your TypeScript journey, embracing structural typing can help you build robust and flexible applications.

Updated on: 21-Aug-2023

185 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements