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
TProps
type parameter allows theComponent
class to infer the type of props passed to the constructor. -
Type-Safe Properties: The class uses
TProps
for theprops
property and thegetProps
method, ensuring strong type safety. -
Dynamic Inference: When creating the
Component
, TypeScript infers the type ofprops
from 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 object
Adding 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
name
orid
). -
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.