Structural typing, interfaces and declaration merging
The compiler asks what a value has, never what it is called; fresh literals, merging and satisfies are the rules around that.
Predict first
Before you read: predict
ExamHall proctor console. This file is compiled with tsc, strict: true.
On which lines does tsc report an error?
interface AlertTile { candidateId: string; severity: number }interface FeedAlert { candidateId: string; severity: number; raisedAt: string } function pin(tile: AlertTile): string { return `pinned ${tile.candidateId}`;} const feedAlert: FeedAlert = { candidateId: "c-41", severity: 2, raisedAt: "09:02" };pin(feedAlert); // line 9pin({ candidateId: "c-42", severity: 3, raisedAt: "09:03" }); // line 10The 30-second version
TypeScript compares shapes, not names: a value fits a type when it has every member the type needs; extras are fine. The exception is an object literal written straight into a typed place, where unknown properties are errors. Same-named interfaces merge, even into a library's module; a type alias never reopens. An annotation retypes the variable; satisfies only checks it.
The compiler asks what a value has, never what it is called; fresh literals, merging and satisfies are the rules around that.
How it works
Step 1 of 12
In the kitchen
- Any worker holding the spec's certificates is sent, whichever agency trained them (step 1) → a value fits a type by its members, not by its type's name
Analogy: A temp-staffing agency matching workers to job specs
A warehouse asks the agency for a forklift driver. The job spec lists the certificates the shift needs, and the agency sends whoever holds them.
| At the agency | In TypeScript |
|---|---|
| Any worker holding the spec's certificates is sent, whichever agency trained them (step 1) | a value fits a type by its members, not by its type's name |
| A worker with extra certificates still qualifies; the warehouse uses only the forklift one (step 2) | extra members pass, unseen through the target |
| The agency's own sign-up form has printed boxes only, and a box written in by hand is refused at the desk (step 4) | a fresh object literal with an unknown property |
| A CV already on file goes out with its extra lines unread (step 5) | an object stored in a variable passes with its extras |
| An addendum to a spec adds requirements, and both lists apply (step 7) | two interface declarations merge |
| A client files an addendum to the agency's standard spec in the agency's own register (step 8) | module augmentation of a library's interface |
| A sealed one-page notice takes no addendum, and a second notice with its number is refused (step 10) | a type alias cannot be declared twice |
| Filing a worker under the spec's headings loses the detail on their CV; checking them against the spec keeps it (steps 11–12) | an annotation against satisfies |
In real terms: tsc compares the members of the value's type with the members of the target type, after merging every interface declaration that shares a name; an object literal written in place is also checked for members the target lacks.
Breaks down when: the agency can check a worker's papers on the day of the shift. The compiler checks nothing when the program runs, because the types are gone from the emitted JavaScript (ts-runtime-boundary-validation).
Contrasting case: a trade guild that sends only its own apprentices, however skilled an outsider is. That is nominal typing, which TypeScript gives you only through a class's private members (step 6).
Examples, run for real
4.1 ExamHall: the pause banner in a missing language
EXH-1520: a proctor console set to the de locale showed a blank Exam paused banner. The banner table was annotated Record<string, string>, so a lookup of a locale the table does not have compiled. MR !1720 declares the tables with satisfies Record<Locale, string> instead.
type Locale = "en" | "fr" | "es";
const pausedBanner: Record<string, string> = { en: "Exam paused", fr: "Examen en pause", es: "Examen en pausa" };
const resumedBanner = { en: "Exam resumed", fr: "Examen repris", es: "Examen reanudado" } satisfies Record<Locale, string>;
const endedBanner = { en: "Exam ended", fr: "Examen terminé" } satisfies Record<Locale, string>;
export const a = pausedBanner.de;
export const b = resumedBanner.de;
4.1 pausedBanner (annotated Record<string, string>): type Record<string, string>; pausedBanner.de -> no diagnostic
4.1 resumedBanner (satisfies Record<Locale, string>): type { en: string; fr: string; es: string; }; resumedBanner.de -> TS2339
4.1 endedBanner (satisfies, no "es" entry): TS1360
The annotation gave the variable the annotated type, whose keys are any string (step 11). satisfies checked the table, reported the one missing a locale, and left the variable with its own three keys (step 12).
4.2 Catalog: search hits shown as product cards
A search returns hits with a relevance score; the grid renders product cards.
interface ProductCard { sku: string; title: string; price: number }
interface SearchHit { sku: string; title: string; price: number; score: number }
declare const hits: SearchHit[];
function renderGrid(cards: ProductCard[]): number { return cards.length; }
renderGrid(hits);
function toCard(h: SearchHit): ProductCard {
return { sku: h.sku, title: h.title, price: h.price, score: h.score };
}
function toThumb(h: SearchHit): ProductCard {
return { sku: h.sku, title: h.title };
}
4.2 renderGrid(hits), SearchHit[] as ProductCard[]: no diagnostic
4.2 toCard returns a literal with score: TS2353
4.2 toThumb returns a literal with no price: TS2741
The array of hits is accepted: every hit has every card member, and the score is simply unseen (steps 1–2). The literal returned by toCard is fresh, so its extra score is an error (step 4). toThumb lacks price (step 3).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
Two category pages keep their search facets in objects. The file is compiled with strict: true:
On which lines does tsc report an error?
type FacetValues = string[] | { min: number; max: number }; const shoeFacets: Record<string, FacetValues> = { brand: ["Acme", "Zenith"], price: { min: 20, max: 180 },};const bagFacets = { brand: ["Acme", "Holt"], price: { min: 15, max: 240 },} satisfies Record<string, FacetValues>; const shoeTop = shoeFacets.price.max; // line 12const bagTop = bagFacets.price.max; // line 13const shoeBrands = shoeFacets.brand.join(", "); // line 14const bagBrands = bagFacets.brand.join(", "); // line 15Question 2 of 5
A payments module declares its types in one file, compiled with strict: true:
On which lines does tsc report an error?
interface Refund { id: string; amount: number } // line 1interface Refund { reason: "damaged" | "late" } // line 2 type Payout = { id: string; amount: number }; // line 4type Payout = { iban: string }; // line 5 const refund: Refund = { id: "r-7", amount: 20, reason: "late" }; // line 7Question 3 of 5
A chat app wants a badge on the member type of a library it installs, and may not edit the library's files. The library's index.d.ts:
The app's src/badge.ts:
import { currentMember } from "@chatkit/core";
export const badge: string | undefined = currentMember().badge;
Compiled with strict: true, tsc reports that badge does not exist on Member.
Which of these, added as the file src/member-badge.ts, makes the project compile?
export interface Member { id: string; name: string }export declare function currentMember(): Member;Question 4 of 5
A podcast player wraps its ids in two classes, so that a track id and an episode id cannot be swapped by mistake. Compiled with strict: true, tsc reports neither mix-up:
Which one change makes tsc report both mix-ups?
class TrackId { constructor(readonly value: string) {}}class EpisodeId { constructor(readonly value: string) {}}function playTrack(id: TrackId): string { return `track ${id.value}`; }function playEpisode(id: EpisodeId): string { return `episode ${id.value}`; } playTrack(new EpisodeId("ep-9")); // mix-up 1playEpisode(new TrackId("tr-3")); // mix-up 2Question 5 of 5
A box office holds a seat while the buyer pays. Compiled with strict: true, tsc reports an error on line 8:
The team wants line 8 to compile, and wants tsc to report a misspelt property name, such as staus, on the line that declares hold.
Which declaration of hold gives both?
type Hold = { status: "held" | "released"; seat: string }; function extendHold(h: { status: "held"; seat: string }): string { return `extend ${h.seat}`;} const hold = { status: "held", seat: "C12" };extendHold(hold); // line 8Go deeper
Pro tips
- When an excess-property error lands on a literal you believe is right, change the target type (add the property, or a union member) rather than moving the literal into a variable to silence the check.
- When it helps: option objects and config literals passed to your own functions (steps 4–5).
- Verify: the handbook's Object Types page: "a majority of excess property errors are actually bugs", and "you might need to revise some of your type declarations".
- Status: DOC.
- To see the type a declaration gave a variable, assign it to
neverfor a moment and read the error.const _see: never = pausedBanner;makestscprint the variable's type in its message.- When it helps: choosing between an annotation and
satisfies(steps 11–12), in CI or anywhere without an editor's hover. - Status: RAN (TypeScript 6.0.3). The message wording is TypeScript's and may change; nothing is graded on it.
- When it helps: choosing between an annotation and
Source
- TypeScript Handbook, "Type Compatibility": https://www.typescriptlang.org/docs/handbook/type-compatibility.html. Quoted: "Type compatibility in TypeScript is based on structural subtyping."; "x is compatible with y if y has at least the same members as x"; "Because JavaScript widely uses anonymous objects like function expressions and object literals, it's much more natural to represent the kinds of relationships found in JavaScript libraries with a structural type system instead of a nominal one."; "if the target type contains a private member, then the source type must also contain a private member that originated from the same class." Supports steps 1–3 and 6, card 10. Valid as of 2026-09-30.
- TypeScript Handbook, "Object Types", section "Excess Property Checks": https://www.typescriptlang.org/docs/handbook/2/objects.html. Quoted: "Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments."; "Since assigning squareOptions won't undergo excess property checks, the compiler won't give you an error"; "a majority of excess property errors are actually bugs". Supports steps 4–5 and tip 1. Valid as of 2026-09-30.
- TypeScript Handbook, "Declaration Merging": https://www.typescriptlang.org/docs/handbook/declaration-merging.html. Quoted: "the merge mechanically joins the members of both declarations into a single interface with the same name"; "If they are not unique, they must be of the same type."; "Then the declarations in an augmentation are merged as if they were declared in the same file as the original." Supports steps 7–8. Valid as of 2026-09-30.
- TypeScript Handbook, "Everyday Types", section "Differences Between Type Aliases and Interfaces": https://www.typescriptlang.org/docs/handbook/2/everyday-types.html. Quoted: "the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable." Supports step 10. Valid as of 2026-09-30.
- TypeScript Handbook, "Modules": https://www.typescriptlang.org/docs/handbook/2/modules.html. Quoted: "any file containing a top-level import or export is considered a module", and "a file without any top-level import or export declarations is treated as a script". Supports step 9. Valid as of 2026-09-30.
- TypeScript 4.9 release notes, "The satisfies Operator": https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html. Quoted: "The new satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression." Supports steps 11–12. Valid as of 2026-09-30.
- Not cited as a rule: what a script file's
declare moduleblock does to an installed package (step 9's second half). No fetched page states it; it rests on onetsc6.0.3 run of the pre-fix P3 option without an import (TS2305,pre-w3b2-fix/logs/practice.out.txt), which no served item now uses.
Related lessons
Checked against the tools and docs as of 2026-09-30.