Lesson 73 of 80
A plugin registers a capability, and the host's own type should grow to know about it — no `any`, no manual bookkeeping. Generics and module augmentation team up to make this happen.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A plugin system has a tension at its core: the host doesn't know at compile time which plugins will be installed, but you still want full type safety for the capabilities each plugin adds. The naive approach — typing the host's plugin registry as Record<string, unknown> — throws that safety away. Every consumer of the host has to cast, and typos in a plugin name fail silently at runtime instead of loudly at compile time.
The fix is to make the host generic over a map of installed plugins, and let each plugin's registration extend that map by one key. The host's type doesn't grow through mutation — it grows through composition, the same way you'd merge object types anywhere else in TypeScript.
// The shape every plugin must conform to.
interface Plugin<Name extends string, Api> {
name: Name;
install: () => Api;
}
// The host starts with an empty capability map.
class App<Registered extends Record<string, unknown> = {}> {
private capabilities = {} as Registered;
use<Name extends string, Api>(
plugin: Plugin<Name, Api>
): App<Registered & Record<Name, Api>> {
const next = this as unknown as App<Registered & Record<Name, Api>>;
(next as any).capabilities = {
...this.capabilities,
[plugin.name]: plugin.install(),
};
return next;
}
get<K extends keyof Registered>(name: K): Registered[K] {
return this.capabilities[name];
}
}
const loggerPlugin: Plugin<"logger", { log: (msg: string) => void }> = {
name: "logger",
install: () => ({ log: (msg) => console.log(`[log] ${msg}`) }),
};
const authPlugin: Plugin<"auth", { currentUser: () => string }> = {
name: "auth",
install: () => ({ currentUser: () => "ada" }),
};
const app = new App().use(loggerPlugin).use(authPlugin);
app.get("logger").log("booted"); // ✅ typed as (msg: string) => void
app.get("auth").currentUser(); // ✅ typed as () => string
app.get("nonexistent"); // ❌ "nonexistent" is not a key of the registered map
Every .use() call returns a new App type whose generic parameter is the old capability map intersected with the new plugin's contribution. By the time you call .get(), TypeScript has been tracking the exact set of installed plugins through the whole chain — app's type genuinely is "an App with logger and auth registered," not just "an App."
App<{ logger: ...; auth: ... }> by hand — each .use() call infers Name and Api from the plugin object passed in, and TypeScript builds the intersection for you..use() returns a new type, not void. A plugin system built around app.register(plugin) returning nothing has no way to teach the compiler about the new capability — the type-level growth requires the fluent, chained-return style.Registered generic exists purely to inform the compiler; nothing about it prevents you from registering the same plugin name twice at runtime and silently overwriting the first one. Add a runtime guard if that's a real risk in your system.This leans on the same constrained-generic mechanics as K extends keyof T from the generic constraints lesson, just applied across a chain of calls instead of a single function. It's also a close cousin of declaration merging and module augmentation — both are ways of teaching TypeScript that a known shape has grown a new member — except here the growth happens through generic inference at each call site rather than through a global declare module block.