Structural Typing: The Duck Test

Intermediate · 21 min read · ▶ live playground · ✦ checkpoint

Structural typing means TypeScript compares the shape a value has, not the name of the type it came from. If an object has the required fields, it belongs in the set, even if nobody declared it with that exact type name.

That is why TypeScript feels flexible with plain objects. It is also why one object literal can be rejected while the same extra field passes after you store it in a variable first.

Shape beats name

Structural typing is a type system rule where compatibility is based on members, not declarations. A badge scanner is the right small picture: it checks whether the badge has the fields on its list, not which printer made the badge.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

SupportProfile and CheckInBadge are different names. TypeScript still accepts supportLead because the value has every field CheckInBadge requires: name, plan, and karma, with compatible types.

You do not have to write implements CheckInBadge anywhere for this plain object. The implements keyword is a class-side promise; Section 6 teaches classes. For object assignability, TypeScript scans the shape.

Extra properties are fine

Assignability is the check that decides whether a value can be used where a target type is expected. For object types, extra properties usually do not hurt assignability. A value can carry more information than the function plans to read.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

storedProfile has role, and ProfileCard does not mention role. That is fine because the assigned value has at least the required fields. The extra field still exists on storedProfile; it is just not visible through the narrower ProfileCard promise.

This rule surprises people coming from name-based languages, but it follows the set model from lesson 1.1. ProfileCard is the set of values with name and plan. Values with more fields can still be inside that set.

Fresh object literals are stricter

An excess property check is TypeScript's extra typo check for a fresh object literal with properties the target type did not ask for. A fresh object literal is an object written directly in the assignment or function call where the target type is known.

Here is the exact check:

type ProfileCard = {
  name: string;
  plan: "free" | "pro";
};
 
function printProfile(profile: ProfileCard): string {
  return profile.name + " uses " + profile.plan;
}
 
console.log(printProfile({
  name: "Mina",
  plan: "pro",
  role: "admin",
}));

→ playground.ts:13:3 - error TS2353: Object literal may only specify known properties, and 'role' does not exist in type 'ProfileCard'.

A non-fresh object is an object value that already lives in a variable before you assign or pass it. Non-fresh values use normal assignability: does the value have the required fields? If yes, extra fields are allowed.

This is the rule behind the confusion:

  • Fresh literal: printProfile({ name: "Mina", plan: "pro", role: "admin" }) gets the excess-property check.
  • Non-fresh value: const storedProfile = ...; printProfile(storedProfile) checks only whether the required shape is present.

The stricter fresh-literal rule catches real bugs. If you write paln: "pro" instead of plan: "pro" in a direct object literal, the checker can point at the typo immediately. Once the object has a separate type and a separate life, TypeScript assumes the extra data may be useful somewhere else.

When shape surprises you

Structural typing is powerful, but it can be too permissive when two domain ideas share the same runtime shape. A user id and an order id may both be strings, but your product does not want to mix them.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

That output is wrong for the product, but TypeScript accepts it because UserId and OrderId are both aliases for the same string set. The checker sees shape, not domain meaning.

A branded type is a future pattern that adds a type-only tag so two values with the same runtime shape stop being assignable to each other. You do not need the pattern yet. Just know the reason it exists: structural typing is intentionally flexible, and sometimes a domain needs names to matter. Lesson 13.3 teaches the full version.

You now have the full object-shape foundation: aliases and interfaces, optional and read-only fields, intersections, and structural assignability. Next section turns to classes, where object shapes meet JavaScript's runtime class syntax.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion