skip to content

You ship a TypeScript library whose published types include `interface RequestOptions`. What can consumers do with that declaration that they could not if you had exported it as a type alias, and why might you not want them to?

level: seniorimportance: should knowfreq 42%

answer

  1. one form is open, the other closed
  2. what can a consumer's own file do?
  3. think about the published .d.ts
  4. keyof stops being final
  5. closedness is what makes exhaustiveness mean something

basics

~20 s

Consumers can add members to an exported interface from their own code by redeclaring it in a module augmentation; an exported type alias is closed and a second declaration is an error. That extensibility is a public commitment you cannot easily withdraw.

solid answer

~50 s

An exported interface is an **open** declaration. A consumer can reopen it from their own project — declaring the same interface inside a `declare module` block for your package — and add members, which the whole compilation then sees. That is how plugin ecosystems let third-party packages teach your types about their additions, and it is genuinely useful for config bags, option objects and registries. It is also a commitment: once shipped, switching that interface to a type alias is a breaking change for anyone who augmented it. And openness costs you certainty inside your own library — `keyof RequestOptions` is no longer final, so exhaustive maps over the key set can be wrong, and the extra members are typed but never validated at runtime, so your code must tolerate properties it has never seen. For a closed model — a variant set you switch on — a type alias, usually a union, is the deliberate choice.

code

typescript · 12 lines
typescript
// The published surface of a small library
export interface RequestOptions {
  url: string;
  timeoutMs?: number;
}

// Closed on purpose: the variant set must stay final
export type Method = "GET" | "POST" | "DELETE";

export function request(method: Method, options: RequestOptions): string {
  return `${method} ${options.url}`;
}

go deeper

for a junior

Know that an interface can be declared more than once and the declarations combine, while a repeated type alias name is an error — and that this matters most for types other people import.

for a middle

Explain that the openness is what lets a consumer add members to your published interface from their own project, and that a type alias offers no equivalent entry point.

for a senior

Weigh both sides in production terms: which shapes you deliberately leave extensible, what you lose in certainty about your own key set, and why a runtime validator that rejects unknown keys contradicts an open published type.

for a principal

Own it as a versioning commitment — going open is easy and going closed again breaks consumers — and set the boundary policy: which parts of the surface advertise extensibility, and which stay closed so exhaustiveness guarantees hold.

## The mechanical difference at the package boundary When you publish declarations, every exported type becomes part of your API surface as surely as every exported function. The interface-versus-alias choice decides one thing about that surface: whether outside code can add to it. An interface is **open**. A consumer who wants an extra field can declare your interface again — inside an augmentation targeting your package — and their declaration contributes members to the same type, everywhere it is used. A type alias is **closed**: a second declaration of the same name in the same scope is a duplicate-identifier error, and there is no other way in. (The mechanics of writing that augmentation are a topic of their own; what matters for the choice is that the door exists in one case and not the other.) ## Why you might want the door open The pattern earns its keep for shapes that are deliberately extensible: - **Option and config bags** where plugins contribute settings your core knows nothing about. - **Registries and maps of names to types**, where each installed package adds its own entry. - **Context or environment objects** that middleware layers decorate as a request flows through. In each case the alternative is worse: forcing consumers to cast, or to fork your types, or to thread a generic parameter through every call. Publishing the shape as an interface is how you say "extend me" in the type layer. ## Why you might not Openness is not free, and the costs land on you rather than on the consumer: **Your key set stops being final.** Anything you derive from the type — `keyof RequestOptions`, a mapped type over its keys, a lookup table keyed by its properties — silently grows in a consumer's compilation. Code that assumed it had handled every key can now be incomplete, and you will not see the failure because it happens in their build, not yours. **The added members are only types.** Nothing validates them at runtime; your library will receive an object carrying properties it has never heard of. If you serialise, forward, log or persist that object, those extra properties travel with it. Your implementation has to be written to tolerate that, and any strict runtime validation you perform will reject what the type layer just promised was legal. **Augmentations are compilation-wide.** Because the augmentation applies to your package's type for the whole program, two dependencies that each add the same property name with different types produce a conflict that neither of them can see and that lands on the poor application author. **It becomes irreversible.** The day you decide `RequestOptions` should be a computed type — a union, or something derived from another type — you break every consumer who augmented it. Shipping an interface is shipping a promise to keep it an interface. ## How to decide Ask what the type *is* in your model: - **An extension point** — options, config, plugin registry, request context: **interface**, and document it as extensible so consumers know it is supported rather than accidental. - **A closed model** — a variant set, a status enumeration, a result union that your own code switches over exhaustively: **type alias**, almost always a union, which cannot be an interface anyway. Closedness is the feature here: it is what makes exhaustiveness checking meaningful. - **A derived type** — computed from another type: **alias**, no choice. A common and good compromise is a union alias whose members are interfaces: the variant set stays closed, so your switches stay honest, while each individual variant remains extensible. ## What separates a strong answer Weak answers stop at "interfaces can be merged". A strong answer treats openness as an API decision with a cost profile: it names who benefits (the consumer, immediately), who pays (you, later, in lost certainty about your own key set and in an inability to change the declaration form), and it identifies the shapes where closedness is a deliberate guarantee rather than an oversight.

  • If you publish a union of object types, can consumers still extend anything about it?
    They cannot add a member to the union itself — a type alias is closed. But if each member of the union is an interface, consumers can extend the individual variants. That combination is often the right shape: a final variant set so your own exhaustive handling stays honest, with extensible payloads inside each variant.
  • Your library validates incoming options at runtime and rejects unknown keys. Does that conflict with publishing them as an interface?
    Yes, directly. The interface tells consumers their augmented properties are legal while the validator throws them out, so the type layer is promising something the implementation refuses. Either loosen the validator to pass unknown keys through, or publish the shape as a closed alias so the type stops advertising extensibility you do not support.
  • Is changing a published type alias into an interface also a breaking change?
    Far less often. Going from closed to open only adds a capability, so existing consumers keep compiling. The dangerous direction is the reverse: once anyone has augmented your interface, turning it into an alias breaks their build, which is why the initial choice deserves thought.

saying these in an interview costs you the question

  • Says the choice only matters for internal code style
  • Assumes augmented properties are validated at runtime
  • Thinks an augmentation is scoped to the file that writes it
  • Claims a published alias can be reopened like an interface
  • Ignores that keyof over an open interface is not final

context