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
154: Abstract Construct Signatures
Imagine you're running a high-end construction firm. You don't actually build every house yourself; instead, you hire subcontractors. But you can't just hire anyone. You have a strict requirement: any subcontractor you hire must provide a "Build Plan" that allows you to kick off a project using a specific set of inputs—like a plot of land and a budget—and that plan must guaranteed to result in a completed House.
You don't care if the subcontractor uses a specific brand of hammer or a specific team of architects. You only care that the process of starting the build (the constructor) follows your rules and produces the right result (the instance). If they can't provide a plan that you can actually "invoke" to create a house, they aren't the right fit for the job.
In TypeScript, that "Build Plan" is exactly what a construct signature is. It's a way to tell TypeScript, "I don't want an instance of a class right now; I want the class itself, and that class must be something I can call new on."
Defining the Requirement to be Instantiable
Usually, when we use an interface, we're describing what an object looks like. But sometimes, we need to describe what a class looks like. This is where the new keyword inside an interface comes in.
Let's say we're building a system that loads different types of database connectors. We want to be able to pass a class into a factory function, and that factory should be able to instantiate it regardless of which database it is.
interface ConnectorConstructor {
new (connectionString: string): DatabaseConnector;
}
interface DatabaseConnector {
connect(): void;
disconnect(): void;
}
Look closely at ConnectorConstructor. It doesn't describe a method; it describes a constructor. By using new (connectionString: string): DatabaseConnector, we are saying: "Any object that satisfies this interface must be a class that can be instantiated with a string and will return an object that adheres to the DatabaseConnector interface."
Passing Classes as Values
This is where people usually get tripped up. There is a huge difference between the type of the instance and the type of the class. If you try to pass a class where TypeScript expects an instance, you'll get a screaming error. Construct signatures solve this by letting us type the class itself.
I've found that the most common mistake is trying to use DatabaseConnector as the type for the factory argument. But DatabaseConnector describes the result of new, not the new call itself.
class PostgresConnector implements DatabaseConnector {
constructor(public connectionString: string) {}
connect() { console.log("Connected to Postgres!"); }
disconnect() { console.log("Disconnected!"); }
}
function createConnection(Ctor: ConnectorConstructor, connStr: string) {
// Because Ctor matches ConnectorConstructor, we know 'new' will work.
return new Ctor(connStr);
}
const myConn = createConnection(PostgresConnector, "postgres://localhost:5432");
myConn.connect();
In the example above, PostgresConnector (the class) is passed as Ctor. Because PostgresConnector has a constructor that takes a string and returns an instance of DatabaseConnector, it satisfies the ConnectorConstructor interface.
When to Use This Over Simple Generics
You might be wondering, "Why not just use a generic?" Well, generics are great for tracking the return type, but they don't inherently force the input to be a constructable class. A construct signature is an explicit contract about the capability of the value being passed.
I use this pattern whenever I'm building plugins, dependency injection containers, or factory patterns. It shifts the validation from runtime (where the app would just crash if you tried to call new on something that wasn't a class) to compile-time. It's a way of saying, "Don't even let the code compile unless you've given me a legitimate class I can instantiate."
📋 Practical Task
Building a Dynamic UI Component Factory
You are building a UI library where components are registered and then instantiated dynamically based on a configuration file. Your task is to implement the construct signature to ensure only valid component classes can be registered.
Requirements:
- Create an interface
Componentthat has a methodrender(): string. - Create an interface
ComponentConstructorthat acts as a construct signature. It must require a constructor that takes apropsobject (as a generic orRecord<string, any>) and returns aComponent. - Implement two classes,
ButtonandTextField, that implementComponent. - Write a function
componentFactory(Ctor: ComponentConstructor, props: any)that returns a new instance of the provided class. - Test your factory by passing in the
Buttonclass and callingrender()on the result.
There are no comments for now.