TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
83: Building a Typed Plugin System
I've seen this happen in countless pull requests: a developer wants to build a plugin system, and their first instinct is to define a Plugin interface with a method like execute(data: any): any. They tell themselves, "I'll just cast the result to the type I need once it comes out of the plugin."
The "Any" Shortcut is a Runtime Time-Bomb
The problem with using any in a plugin system is that you aren't actually building a typed system; you're just building a JavaScript system and putting a TypeScript wrapper around it. You lose the very thing that makes TS valuable: the guarantee that the data flowing from Plugin A into Plugin B is actually what Plugin B expects.
// The "Wrong" Way
interface Plugin {
name: string;
execute(data: any): any;
}
const plugins: Plugin[] = [
{ name: 'UpperCase', execute: (d) => d.toUpperCase() },
{ name: 'LengthCounter', execute: (d) => d.length }
];
// This looks fine to TS, but if the order changes,
// the 'UpperCase' plugin will try to call .toUpperCase() on a number.
// BOOM. Runtime error.
plugins.reduce((acc, plugin) => plugin.execute(acc), "hello");
If you rely on any, you're essentially playing Russian Roulette with your data pipeline. You might get lucky in development, but your users will find the edge cases where a plugin returns an unexpected shape and crashes the entire application.
Defining Type-Safe Contracts with Generics
To do this right, we need the plugin system to be aware of the transformation happening. Instead of a generic "execute" method, we define our plugins using generics. This allows each plugin to declare exactly what it consumes and what it produces, while still adhering to a common structure.
Let's imagine we're building a Log Processing Engine. We want plugins that can filter logs, transform them, or enrich them with metadata.
interface Plugin<TInput, TOutput> {
name: string;
process(input: TInput): TOutput;
}
// A plugin that takes a raw string log and returns a structured object
const LogParser: Plugin<string, { message: string; timestamp: number }> = {
name: 'Parser',
process: (input) => ({
message: input,
timestamp: Date.now(),
}),
};
// A plugin that takes that object and adds a severity level
const SeverityTagger: Plugin<{ message: string; timestamp: number }, { message: string; timestamp: number; level: string }> = {
name: 'Tagger',
process: (input) => ({
...input,
level: input.message.includes('ERROR') ? 'high' : 'low',
}),
};
By using TInput and TOutput, we've created a contract. TypeScript can now verify that the output of LogParser matches the input of SeverityTagger. If I tried to swap their order, the compiler would scream at me immediately.
Managing a Heterogeneous Plugin Registry
Now, here is where it gets tricky. You usually don't just have one plugin; you have a list of them. But if every plugin has different generic types, a simple array like Plugin<any, any>[] puts us right back where we started with any.
The secret is to create a "Pipeline" wrapper. The pipeline doesn't care about the internal types of every single plugin; it only cares that the start of the chain matches the initial input and the end matches the final output. I personally prefer using a builder pattern for this to maintain type safety across the chain.
class PluginPipeline<TCurrent> {
constructor(private pipeline: ((input: TCurrent) => TCurrent) = (i) => i) {}
pipe<TNext>(plugin: Plugin<TCurrent, TNext>): PluginPipeline<TNext> {
return new PluginPipeline<TNext>((input) => {
const result = this.pipeline(input as any); // Internal cast is safe here because of the signature
return plugin.process(result);
});
}
execute(input: TCurrent): any {
return this.pipeline(input);
}
}
// Usage
const myPipeline = new PluginPipeline<string>()
.pipe(LogParser)
.pipe(SeverityTagger);
const finalLog = myPipeline.execute("ERROR: System failure");
// finalLog is now typed as { message: string; timestamp: number; level: string }
Notice how the pipe method returns a PluginPipeline<TNext>. This "shifts" the type of the pipeline forward. It's a bit of a dance with the type system, but it ensures that you can't accidentally plug a "Number-to-String" plugin into a "Boolean-to-Date" slot.
📋 Practical Task
Exercise: Implementing a Typed Image Processing Pipeline
You are building a tool that processes image metadata. You need to create a type-safe plugin system where images move through a series of transformations.
Requirements:
- Define a
MetadataPlugin<TIn, TOut>interface with atransform(input: TIn): TOutmethod. - Create three specific plugins:
RawToBasic: Takes astring(raw file path) and returns an object{ path: string, size: number }.BasicToDetailed: Takes the basic object and adds{ dimensions: { w: number, h: number } }.DetailedToJson: Takes the detailed object and returns astring(JSON representation).
- Implement a
Pipelineclass similar to the one in the lesson that allows these plugins to be chained using a.add()method. - Ensure that if you try to add
RawToBasicafterDetailedToJson, TypeScript throws a compilation error. - Execute the pipeline starting with the string
"image.jpg"and verify the final output is astring.
There are no comments for now.