Skip to Content
Course content

154: Abstract Construct Signatures

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Component that has a method render(): string.
  • Create an interface ComponentConstructor that acts as a construct signature. It must require a constructor that takes a props object (as a generic or Record<string, any>) and returns a Component.
  • Implement two classes, Button and TextField, that implement Component.
  • Write a function componentFactory(Ctor: ComponentConstructor, props: any) that returns a new instance of the provided class.
  • Test your factory by passing in the Button class and calling render() on the result.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.