Skip to main content
CodeOath
← All posts

TypeScript63 min total · 21 parts

TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime

Part 4 of 21 · ~2 min

Structural Typing: TypeScript Checks Shape, Not Names

interface Applicant {
  id: number;
  name: string;
  email: string;
}

function printApplicant(a: Applicant) {
  console.log(`${a.name} <${a.email}>`);
}

const webhookPayload = {
  id: 8841,
  name: "Priya Shah",
  email: "priya@example.com",
  source: "greenhouse",
  resumeUrl: "https://...",
};

printApplicant(webhookPayload); // no error

webhookPayload was never declared as an Applicant, and TypeScript let the call through anyway. If you're coming from a language where a class has to explicitly say implements SomeInterface before it counts as one, this looks like it should be an error and isn't. TypeScript doesn't check what a value calls itself — it checks whether the value's own fields cover what the function is asking for, and stops looking the moment they do. webhookPayload has an id, a name, and an email matching Applicant's shape, and the extra fields the ATS vendor tacked on don't disqualify it. This is structural typing (sometimes called "duck typing," borrowed from the same idea in plain JavaScript, applied at compile time instead of runtime). It's why a webhook payload full of vendor-specific extras can be handed straight to code that only knows about Applicant — nobody has to strip it down to a clean shape first.

There's one specific case where this generosity disappears, and it catches people constantly:

function scheduleInterview(config: { applicantId: number; slot: string }) {
  /* ... */
}

scheduleInterview({ applicantId: 8841, slot: "2026-09-15T10:00", notes: "phone screen" });
// Error: Object literal may only specify known properties, and 'notes'
// does not exist in the expected type.

const interviewConfig = { applicantId: 8841, slot: "2026-09-15T10:00", notes: "phone screen" };
scheduleInterview(interviewConfig); // no error

Both calls hand scheduleInterview a value with an extra notes field it never asked for, and structural typing alone says both should be fine — an extra field doesn't stop the required ones from matching. The first call still fails, because TypeScript treats a freshly-written object literal differently from a value that already has a name: a literal carrying a property nobody asked for is more likely a typo (nots meant as notes, in a shape that expected neither) than a deliberate addition, so TypeScript flags it right there, before it can hide inside a variable. Move the same object into interviewConfig first, and the check disappears — not because the shape changed, but because it's no longer a bare literal at the call site. webhookPayload reached printApplicant through a variable too, a few lines up — which is exactly why its extra fields never tripped this same check.