Typescript Generics

Furkan Çağlar Gülmez
Picus Security Engineering
8 min readDec 18, 2023
Photo by Clément Hélardot on Unsplash

What are Typescript Generics?

With Typescript Generics, you may keep the type information while creating reusable components. In essence, generics let us create reusable, flexible functions, classes, or interfaces that can handle a wide range of data types.

All that a TypeScript generic type is really is a placeholder for a particular type. It enables us to define components so that, upon usage, the caller provides the component’s types. This versatility makes it feasible to write code that is more reliable and flexible.

General Use Cases

  • Array Functions
    — Generics are commonly used in functions that manipulate arrays. For instance, a function to extract the first element of an array without specifying the type explicitly.
// Generic function to get first element of an array with generic type
function getFirstElement<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
  • Data Structures
    — Generic types are instrumental in creating flexible data structures like stacks, queues, or linked lists that can handle different types of data.
// Stack data structure using generics
class Stack<T> {
private items: T[];
    constructor() {
this.items = [];
}
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
  • Utility Functions
    — They’re valuable in creating utility functions that work with diverse data types without the need for redundant code.
// Utility function to filter an array based on a condition
function filterArray<T>(arr: T[], condition: (item: T) => boolean): T[] {
return arr.filter(item => condition(item));
}

Advantages of Generics and Why Should We Use Them?

Generics are a powerful and flexible tool in programming languages. When writing code, a variety of advantages come with using generics, optimizing the development process and improving code quality. Here are some of these advantages:

  • 1️⃣ Reducing Code Duplication: Generics prevent code repetition by defining structures that work with different data types. Instead of writing separate functions that share the same logic but operate on different data types, generic structures can be used. This reduces code repetition and eases maintenance.
  • 2️⃣ Type Safety: Generics enhance type safety by defining data types, reducing the risk of working with unexpected types. They enable early detection of errors during compile time, making the program more robust and secure.
  • 3️⃣ Flexibility and General Usage: Generics are versatile, applicable across a wide range of areas including data structures, functions, and classes. This makes code more general and reusable.
  • 4️⃣ Performance and Optimization: Generics can offer performance advantages. Defining data types can optimize operations during compile time. Avoiding unnecessary type conversions or uncertainties can boost performance.
  • 5️⃣ Code Readability and Organization: Generics can enhance code readability. Generic structures make code more organized and comprehensible. Following specific design patterns with generics provides a standard structure while organizing the codebase.

Generics play a significant role in software development, providing developers with flexibility, security, and performance. These advantages are particularly crucial in large-scale projects and modular coding approaches.

Disadvantages of Generics and When Should We Not Use Them?

TypeScript generics offer powerful features, but they also come with certain limitations and scenarios where their usage might not be optimal.

  • 1️⃣ Increased Complexity: In simpler projects or scenarios where the use of generics would add unnecessary complexity, opting for straightforward type definitions might be more appropriate. Overusing generics in simple cases can overcomplicate the codebase.
  • 2️⃣ Learning Curve: Teams with limited experience or familiarity with generics might face a learning curve. In such cases, using generics extensively without a solid understanding could lead to errors and maintenance issues.
  • 3️⃣ Over-Abstraction: Overusing generics to create overly abstract or complex data structures might reduce code readability and hinder understanding, especially for newcomers to the codebase.
  • 4️⃣ Performance Impact: Generics might introduce a minor performance overhead due to additional type checking and compilation processes. In performance-critical applications, this overhead might be a consideration.
  • 5️⃣ Debugging and Error Messages: Error messages involving generics can sometimes be complex and hard to decipher, especially for those not well-versed in TypeScript. This could pose challenges during debugging and troubleshooting.
  • 6️⃣ Code Bloating: Over-genericizing code can lead to code bloat. Unnecessary abstraction and overuse of generics might inflate the codebase, making it harder to maintain.
  • 7️⃣ Limited Support in Some Scenarios: Certain scenarios or external libraries might not fully support generics, leading to compatibility issues. In such cases, using generics extensively might not be feasible.

While TypeScript generics offer significant advantages, their usage should be balanced based on project complexity, team expertise, and specific requirements. They are powerful tools, but not all scenarios necessarily benefit from their application. Understanding when to leverage generics and when to resort to simpler type definitions is crucial for maintaining a clear and maintainable codebase.

Basic Generics Example, Usage and Benchmark Test

class GenericStack {
private items: any[];
    constructor() {
this.items = [];
}
push(item: any): void {
this.items.push(item);
}
pop(): any {
return this.items.pop();
}
peek(): any {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
class Stack {
private items: number[];
constructor() {
this.items = [];
}
push(item: number): void {
this.items.push(item);
}
pop(): number | undefined {
return this.items.pop();
}
peek(): number {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// Time Calculation w/ Generic Stack
function measureTimeForStackOperationsGenerics() {
const startTime = performance.now(); // Start Time
const numberStack = new GenericStack();
for (let i = 0; i < 1000000; i++) {
numberStack.push(i);
}
for (let i = 0; i < 1000000; i++) {
numberStack.pop();
}
const endTime = performance.now(); // End Time
return endTime - startTime; // Calculate Elapsed Time
}
const elapsedTimeGenerics = measureTimeForStackOperationsGenerics();
console.log("Elapsed time w/ Generic Stack (milliseconds)", elapsedTimeGenerics);
// Time Calculation w/ Normal Stack
function measureTimeForStackOperations() {
const startTime = performance.now(); // Start Time
const numberStack = new Stack();
for (let i = 0; i < 1000000; i++) {
numberStack.push(i);
}
for (let i = 0; i < 1000000; i++) {
numberStack.pop();
}
const endTime = performance.now(); // End Time
return endTime - startTime; // Calculate Elapsed Time
}
const elapsedTime = measureTimeForStackOperations();
console.log("Elapsed time (milliseconds):", elapsedTime);
  • Run 1️⃣
[LOG]: "Elapsed time w/ Generic Stack (milliseconds)",  11.300000011920929 
[LOG]: "Elapsed time (milliseconds):", 11.800000011920929
  • Run 2️⃣
[LOG]: “Elapsed time w/ Generic Stack (milliseconds)”, 10.799999952316284
[LOG]: “Elapsed time (milliseconds):”, 15.800000011920929
  • Run 3️⃣
[LOG]: “Elapsed time w/ Generic Stack (milliseconds)”, 12.900000035762787
[LOG]: “Elapsed time (milliseconds):”, 10.399999976158142

For a performance comparison between a Generic Stack implementation and a Normal Stack without generics, I conducted multiple runs to measure the elapsed time for stack operations.

Observations:

  • In Run 1, the Generic Stack showcased slightly better performance compared to the Normal Stack, with a marginal difference of 0.5 milliseconds.
  • Run 2 demonstrated a more significant performance gap, indicating that the Generic Stack outperformed the Normal Stack by approximately 5 milliseconds.
  • Run 3 saw a reversal in the results, where the Normal Stack exhibited faster performance, completing operations 2.5 milliseconds ahead of the Generic Stack.

Analysis:

  • The performance variations observed across runs suggest that the effectiveness of generics in stack operations can vary based on different scenarios or data loads.
  • While the Generic Stack generally showed competitive or slightly better performance, the Normal Stack occasionally outperformed it, hinting at contextual dependencies.
  • It’s important to consider that these benchmarks represent specific test cases and may not universally reflect the performance in all scenarios.

Generics in the Stack implementation displayed comparable or slightly better performance in most cases, providing a reusable and type-safe solution.

However, the occasional outperformance by the Normal Stack signifies that the use of generics should be evaluated based on specific application requirements and performance considerations.

These benchmark results indicate that while generics often provide efficient and type-safe solutions, their performance may vary based on contextual factors and specific use cases. The decision to use generics should be made considering these factors to ensure an optimal balance between flexibility and performance.

Advanced Generics: In-depth Exploration

Generics in TypeScript offer more than just basic type flexibility; they provide powerful tools to create intricate and precise type systems. Let’s delve into some advanced concepts:

1️⃣ Generic Constraints:

Generic constraints limit the types that a generic function or class can operate on.

function lengthOf<T extends { length: number }>(obj: T): number {
return obj.length;
}

This constrains the function to accept only objects that have a length property of type number.

2️⃣ Conditional Types:

Conditional types allow creating types that depend on other types.

type Check<T> = T extends string ? boolean : number;
let a: Check<string>; // 'a' will be of type boolean
let b: Check<number>; // 'b' will be of type number

Conditional types enable defining types based on conditions, offering conditional behavior in type definitions.

3️⃣ Mapped Types:

Mapped types transform properties in one type to another.

type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

Mapped types can create new types from existing ones, such as making all properties of a type mutable.

Advanced Use Cases:

  • Intersection and Union Types: Combining types for more complex scenarios.
  • Distributive Conditional Types: Behavior when conditional types are applied to a union of types.
  • Infer Keyword: Extracting types within conditional type checks.

Benefits:

  • Advanced generics empower developers to create precise, reusable, and sophisticated type structures.
  • They enhance type safety, code readability, and maintainability, especially in complex scenarios.
  • These features allow for creating flexible and intricate type systems tailored to specific use cases.

Considerations:

  • While powerful, advanced generics can increase code complexity. Careful design and documentation are crucial for readability.
  • Overuse of complex generic types might hinder readability and understanding for developers new to the codebase.

Advanced generics in TypeScript offer a spectrum of capabilities to create sophisticated type systems. They provide a robust foundation for crafting expressive and precise type definitions, enabling developers to build more scalable and maintainable applications.

This exploration illustrates the capabilities and considerations of advanced generics in TypeScript, highlighting their value in building flexible and robust type systems while acknowledging potential complexities and the need for balanced usage.

Conclusion

In the realm of TypeScript, generics stand as a powerful tool, offering a blend of flexibility and type safety. By allowing functions, classes, and interfaces to work with various data types while maintaining type checks, generics significantly reduce redundancy in code. They enable developers to create reusable components that adapt to different data structures, promoting modularity and enhancing the maintainability of the codebase. TypeScript generics empower developers to build adaptable solutions without compromising type safety, fostering a more robust and scalable code architecture.

Thanks for reading. If you have questions or comments regarding this article, please feel free to leave a comment below. Also, don’t hesitate to add any other potential solutions that may come into your mind about this topic
👐

--

--