Having Fun with TypeScript: Structural Typing

Types Based on Shapes, not Names

Dagang Wei
4 min readMar 18, 2024
Source

This blog post is part of the series Having Fun with TypeScript.

Introduction

TypeScript, as many developers love, introduces a layer of type safety over the dynamic nature of JavaScript. This boosts code reliability, maintainability, and provides a better developer experience. One of the core concepts that makes TypeScript’s type system powerful is structural typing. Let’s dive into what this means.

Types Based on Structure, Not Names

In contrast to nominal type systems (like in languages such as Java or C#), structural typing doesn’t focus on the explicit names of types. Instead, it determines type compatibility based on the structure (i.e., the properties and methods) an object possesses. Let’s see this in action:

Interfaces

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

interface Developer {
name: string;
age: number;
codingLanguages: string[];
}
let john: Person = { name: "John Doe", age: 30 };
let jane: Developer = { name: "Jane Smith", age: 28, codingLanguages: ["TypeScript", "Python"] };
// Perfectly valid!
john = jane;

Even though Person and Developer are distinct interfaces, we can assign a Developer object to a variable of type Person. This works because they both share the properties name and age.

Anonymous Types

function calculateArea(shape: { width: number, height: number }) {
return shape.width * shape.height;
}

let rectangle = { width: 5, height: 10 };
console.log(calculateArea(rectangle)); // Output: 50

Function Types

Structural typing applies to function types as well. TypeScript focuses on the input and output types of functions. As long as they match, functions can be used interchangeably, regardless of whether they are explicitly declared to have a particular function type.

// Type alias for a callback expecting a number and returning a string
type NumberToStringCallback = (num: number) => string;

function executeCallback(callback: NumberToStringCallback) {
const result = callback(42);
console.log(result);
}
// Function 1: Explicitly matches the NumberToStringCallback type
const formatNumber: NumberToStringCallback = (num) => "The number is: " + num;
executeCallback(formatNumber); // Output: The number is: 42
// Function 2: Structural compatibility - also works!
const addSuffix = (num: number) => "My number is " + num + "!";
executeCallback(addSuffix); // Output: My number is 42!

Benefits of Structural Typing

Structural typing offers several advantages, making TypeScript an appealing choice for developers:

  • Flexibility: It allows for more flexible code without sacrificing type safety. You can pass objects around that weren’t explicitly declared to implement an interface, as long as they match the structure.
  • Code Reusability: It facilitates code reuse. Interfaces can be satisfied by many different objects, making it easier to write generic and reusable code.
  • Ease of Refactoring: Refactoring is simpler since you can change the name of a type without affecting its compatibility with other types, as long as the structure remains the same.
  • Mocking and Testing Made Easy: Structural typing simplifies creating mock objects for testing. Instead of replicating an entire class, you only need to provide the relevant properties or methods for your test scenario.

Potential Drawbacks

While structural typing has its advantages, there are scenarios where nominal typing might be preferred:

  • Intent Clarity: Nominal typing can sometimes make the intended use of a type clearer. With structural typing, it’s possible to inadvertently satisfy an interface, potentially leading to confusion or misuse.
  • Strictness: There might be cases where you want to ensure that an object not only has the right shape but also comes from a specific lineage or class. Structural typing’s flexibility could be a drawback in these scenarios.

Key Points to Consider

  • Excess Properties Are Allowed: When assigning to a type, the object can have more properties than what the type defines. TypeScript only cares about the presence of the required properties.
  • Classes Are Structural Too: While classes play a role in TypeScript, remember that instances of those classes are also compared structurally when it comes to type compatibility.
  • A Touch of Unsoundness: TypeScript, to maintain its JavaScript compatibility, occasionally allows operations that might not perfectly align with type safety at compile time. These are carefully considered edge cases that generally make developers’ lives easier.

In Summary

Structural typing in TypeScript offers a powerful model for type compatibility based on shapes. It strikes a balance between flexibility and type safety, allowing developers to write more generic, reusable code. By understanding and leveraging this concept, you can enhance your TypeScript projects, making them more maintainable and robust. As with any design decision, it’s essential to weigh the benefits and potential drawbacks to determine the best approach for your specific use case.

--

--