Leveraging Generics in TypeScript Classes: A Practical Guide

Generics in TypeScript provide a powerful mechanism to create reusable, type-safe components. This article explores how to apply generics effectively in a class, including extending generic constraints to enforce specific structures.
The Problem: Inferring Types Dynamically
Imagine we have a Component class that takes some properties and exposes a method to retrieve them. Here’s an example:
export class Component<TProps> {
private props: TProps;
constructor(props: TProps) {
this.props = props;
}
getProps = () => this.props;
}
// Example usage:
const component = new Component({ a: 1, b: 2, c: 3 });
const result = component.getProps(); // result: { a: number; b: number; c: number }Breaking Down the Code
-
Generic Slot: The
TPropstype parameter allows theComponentclass to infer the type of props passed to the constructor. -
Type-Safe Properties: The class uses
TPropsfor thepropsproperty and thegetPropsmethod, ensuring strong type safety. -
Dynamic Inference: When creating the
Component, TypeScript infers the type ofpropsfrom the object passed to the constructor.
Extending the Generic Prop with Constraints
We can enforce that the generic type must meet certain criteria using constraints. For example, we can ensure that props is always an object.
export class Component<TProps extends object> {
constructor(props: TProps) {
this.props = props;
}
private props: TProps;
getProps = () => this.props;
}
// Example usage:
const component = new Component({ a: 1, b: 2 });
const result = component.getProps(); // result: { a: number; b: number }
// This will cause a TypeScript error:
// const invalidComponent = new Component(42); // Error: number does not extend objectAdding Specific Constraints
Let’s say we want to ensure that every props object has a name property of type string. We can extend the constraint like this:
export class Component<TProps extends { name: string }> {
constructor(props: TProps) {
this.props = props;
}
private props: TProps;
getProps = () => this.props;
}
// Example usage:
const component = new Component({ name: "MyComponent", age: 30 });
const result = component.getProps(); // result: { name: string; age: number }
// This will cause a TypeScript error because 'name' is missing:
// const invalidComponent = new Component({ age: 30 });Real-World Example: Using Base Types
You can also use a predefined base type to enforce consistent structure while allowing additional fields:
type BaseProps = { id: string };
export class Component<TProps extends BaseProps> {
constructor(props: TProps) {
this.props = props;
}
private props: TProps;
getProps = () => this.props;
}
// Example usage:
const component = new Component({ id: "abc123", customField: 42 });
const result = component.getProps(); // result: { id: string; customField: number }
// This will cause a TypeScript error if 'id' is missing:
// const invalidComponent = new Component({ customField: 42 });Why Extend Generics?
Extending generics provides a way to balance flexibility and type safety. It ensures that:
-
Specific constraints are met (e.g., required fields like
nameorid). -
Type inference still works dynamically for additional fields.
-
Code remains reusable and maintainable.
Conclusion
Generics in TypeScript, combined with constraints, allow developers to build reusable and type-safe components that adapt to various structures while enforcing specific rules. This approach is especially useful in libraries, APIs, and frameworks, ensuring robust and predictable behavior in your code.
