Skip to Content
Course content

6: Understanding Structural Typing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed that developers coming from languages like Java or C# often hit a wall when they first encounter how TypeScript handles types. They're used to "Nominal Typing," where if you say a function needs a User object, you must pass it something that was explicitly declared as a User. TypeScript doesn't work like that. It uses structural typing.

Think of it like a USB-C port on your laptop. Your laptop doesn't care if the cable you plug in is branded by Apple, Samsung, or some random company from Amazon. It doesn't ask the cable for its "birth certificate" or "identity papers." It only cares about one thing: Does the plug fit the hole? If the physical shape (the structure) matches the port, the connection works. The identity of the cable is irrelevant; only its capabilities matter.

The "If it fits, it sits" Logic

In TypeScript, the "shape" of your data is what defines its type. If you define a type that requires a name and an email, any object that has at least those two properties will satisfy that type, regardless of where that object came from or what it's called.

interface Person {
  name: string;
  email: string;
}

function sendWelcomeEmail(user: Person) {
  console.log(`Sending mail to ${user.name} at ${user.email}`);
}

// This is a 'Customer', not a 'Person'
const customer = {
  name: "Alice",
  email: "alice@example.com",
  customerId: 12345, // Extra property
  loyaltyPoints: 500  // Extra property
};

// This works perfectly!
sendWelcomeEmail(customer);

In a nominal system, the line sendWelcomeEmail(customer) would throw a compiler error because customer wasn't explicitly labeled as a Person. But in TypeScript, the compiler looks at customer and says, "Well, it has a name (string) and an email (string). That's all I asked for. I don't care that it also has a customerId. It fits the port."

Why Extra Properties Are Usually Fine

You might wonder why TypeScript allows customer to have extra properties like loyaltyPoints. The reason is flexibility. In real-world software, your data often evolves. You might have a base set of requirements for a function, but the objects flowing through your system are often richer than what a single function needs to know about.

This makes your code much more decoupled. I can write a Logger that only needs an object with a message property, and I can pass it a full Error object, a Request object, or a simple string-wrapper without having to write complex inheritance hierarchies just to make the types match.

The One Catch: Fresh Object Literals

Here is a quirk that trips people up: TypeScript is more strict when you pass an object literal directly into a function. I call this "Excess Property Checking."

// This will throw a TS error!
sendWelcomeEmail({
  name: "Bob",
  email: "bob@example.com",
  phoneNumber: "555-0123" // Error: 'phoneNumber' does not exist in type 'Person'
});

Wait, why did the customer object work, but this literal doesn't? It's because when you pass a literal, TypeScript assumes you're making a mistake. It figures that if you're typing out a brand new object right there in the function call, and you're adding properties that the function doesn't use, you probably have a typo or you're passing the wrong object entirely. It's a safety feature to prevent you from misspelling email as emall and wondering why the value is undefined.

Once that object is assigned to a variable (like we did with customer), TypeScript relaxes and goes back to pure structural checking. It's a subtle distinction, but it's the difference between a build that passes and one that screams at you.




📋 Practical Task

Refactoring the Video Player Compatibility Layer

You are working on a media app. You have a function called playMedia that requires an object with a url and a duration. However, your app handles different types of media (Videos and Audio clips) which have different properties.

Your Task: Implement the playMedia function and the corresponding types so that it can accept both a VideoFile and an AudioFile without using a union type (like VideoFile | AudioFile). Instead, rely on structural typing to ensure any object with the minimum required properties is accepted.

// 1. Define a type or interface called 'MediaElement' 
//    that contains the minimum requirements for playMedia.

// 2. Create a 'VideoFile' type that includes 'url', 'duration', and 'resolution'.
// 3. Create an 'AudioFile' type that includes 'url', 'duration', and 'bitrate'.

function playMedia(media: /* Your type here */) {
  console.log(`Playing ${media.url} for ${media.duration} seconds.`);
}

const myVideo = {
  url: "https://assets.com/intro.mp4",
  duration: 120,
  resolution: "1080p"
};

const myAudio = {
  url: "https://assets.com/podcast.mp3",
  duration: 3600,
  bitrate: "320kbps"
};

// Test your implementation
playMedia(myVideo);
playMedia(myAudio);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.