Hands‑On Exercise: Build a Typed Plugin System Using Interfaces¶
We’ll create a tiny “application core” that loads plugins.
Each plugin must implement a shared interface, but can behave however it wants internally.
This is a perfect demonstration of:
- enforcing contracts
- polymorphism
- interface‑driven architecture
- class‑based plugin implementations
Step 1 — Define the Plugin Interface¶
Create:
plugin.ts¶
export interface Plugin {
name: string
initialize(): void
execute(data: string): string
}
This interface defines the contract every plugin must follow.
name→ identifies the plugininitialize()→ setup logicexecute()→ transforms input data
Any plugin that doesn’t implement these correctly will fail at compile time.
Step 2 — Create the Plugin Manager¶
Create:
pluginManager.ts¶
import { Plugin } from "./plugin"
export class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin) {
this.plugins.push(plugin)
}
initializeAll() {
for (const p of this.plugins) {
p.initialize()
}
}
runAll(data: string): string[] {
return this.plugins.map(p => p.execute(data))
}
}
This class:
- stores plugins
- initializes them
- runs them in sequence
- returns their outputs
Because of the interface, the manager doesn’t care which plugin it receives—only that it satisfies the contract.
Step 3 — Implement Concrete Plugins¶
Create:
uppercasePlugin.ts¶
import { Plugin } from "./plugin"
export class UppercasePlugin implements Plugin {
name = "uppercase"
initialize() {
console.log("Uppercase plugin initialized")
}
execute(data: string) {
return data.toUpperCase()
}
}
Another plugin:
reversePlugin.ts¶
import { Plugin } from "./plugin"
export class ReversePlugin implements Plugin {
name = "reverse"
initialize() {
console.log("Reverse plugin initialized")
}
execute(data: string) {
return data.split("").reverse().join("")
}
}
Both plugins implement the same interface, but behave differently.
This is polymorphism powered by TypeScript’s structural typing.
Step 4 — Use the Plugin System¶
Create:
index.ts¶
import { PluginManager } from "./pluginManager"
import { UppercasePlugin } from "./uppercasePlugin"
import { ReversePlugin } from "./reversePlugin"
const manager = new PluginManager()
manager.register(new UppercasePlugin())
manager.register(new ReversePlugin())
manager.initializeAll()
const results = manager.runAll("hello world")
console.log(results)
Run it:
Uppercase plugin initialized
Reverse plugin initialized
[ 'HELLO WORLD', 'dlrow olleh' ]
Your plugin system works.
Step 5 — Add a Third‑Party Plugin (Structural Typing Magic)¶
Here’s the twist:
A plugin does not need to explicitly implement the interface.
If it has the right shape, TypeScript accepts it.
customPlugin.ts¶
export const CustomPlugin = {
name: "custom",
initialize() {
console.log("Custom plugin ready")
},
execute(data: string) {
return `*** ${data} ***`
}
}
Use it:
import { CustomPlugin } from "./customPlugin"
manager.register(CustomPlugin) // ✔ Works due to structural typing
This is incredibly powerful for plugin ecosystems.
Step 6 — Add Optional Capabilities (Advanced)¶
Let’s extend the interface with optional hooks.
Modify plugin.ts:
export interface Plugin {
name: string
initialize(): void
execute(data: string): string
cleanup?(): void
}
Plugins may implement cleanup, but don’t have to.
Update the manager:
shutdown() {
for (const p of this.plugins) {
p.cleanup?.()
}
}
This uses optional chaining to call the hook only if it exists.
Step 7 — Add a Plugin That Uses cleanup¶
import { Plugin } from "./plugin"
export class LoggingPlugin implements Plugin {
name = "logger"
initialize() {
console.log("Logger started")
}
execute(data: string) {
console.log("Processing:", data)
return data
}
cleanup() {
console.log("Logger shutting down")
}
}
Now the system supports lifecycle hooks—just like real plugin frameworks.
What You Learned¶
This hands‑on exercise demonstrates how TypeScript makes plugin architectures safe and expressive.
1. Interfaces enforce contracts¶
- Plugins must implement required methods
- Compile‑time errors catch missing or incorrect implementations
2. Classes provide structure¶
- Encapsulation
- Reusable logic
- Clear initialization and execution flows
3. Structural typing enables flexible polymorphism¶
- Plugins don’t need to explicitly
implementthe interface - Any object with the right shape works
- Perfect for third‑party extensions
4. Optional members allow extensibility¶
- Add new capabilities without breaking existing plugins
- Real‑world pattern used in frameworks and libraries
This is exactly how real plugin systems are built in TypeScript ecosystems.