DEV Community

ultroNeous Technologies
ultroNeous Technologies

Posted on

Enhancing TypeScript's Power: Unleashing the Practical Use of Generics

In this article, I will show you some real-life examples of how Generics are used, based on my own experience. This way, it'll make more sense to you and be easier to understand. I've also divided the usage of Generics into different categories that make sense to me.

Introduction to TypeScript Generics

Understanding the basics of TypeScript generics

TypeScript generics provide developers with the ability to create flexible and reusable code by introducing the concept of parameterizing types.

In other words, Whenever you're creating a component, there may be instances where you're unsure about the types you'll need. However, you have the flexibility to pass the types when you use the component later on. This allows for greater adaptability and customization in your code.

Exploring Generic Functions in TypeScript

Defining and using generic functions

In TypeScript, generic functions are defined by including a type parameter, enclosed in angle brackets <> before the function name. This type parameter represents the generic type that will be used within the function.

function helloGenerics<T>(arg: T): T {
   return arg;
}
Enter fullscreen mode Exit fullscreen mode

Suppose we have a scenario where we need to create a function that can determine the length of an array, regardless of the type of elements it contains. In such cases, we can utilise generics to accomplish this task.

function getArrayLength<T>(array: T[]): number {
  return array.length;
}

const numbers = [1, 2, 3];
const strings = ['hello', 'generics'];

console.log(getArrayLength(numbers)); // Output: 3
console.log(getArrayLength(strings)); // Output: 2
Enter fullscreen mode Exit fullscreen mode

Leveraging type inference with generic functions

Now, let's imagine that you want to enhance the versatility of the function called identity. You wish to modify it in such a way that it can accept arguments of any type, be it a number, string, or boolean. To achieve this, you might consider adopting the following approach:

// Without Using Generics
function helloGenerics(arg: string | number | boolean) {
  return arg;
}

console.log(helloGenerics(10)); // Output: 10
console.log(helloGenerics("hello generics")); // Output: hello generics
console.log(helloGenerics(true)); // Output: true


// With Generic
function helloGenerics<T>(arg: T): T {
  return arg;
}

console.log(helloGenerics(10)); // Output: 10
console.log(helloGenerics("hello generics")); // Output: hello generics
console.log(helloGenerics(true)); // Output: true
Enter fullscreen mode Exit fullscreen mode

In the example provided above, when we don't use generics, we have to explicitly specify the types of parameters we want to accept. However, by leveraging the power of generics, we can simplify this process. With generics, whenever we call the function with any type of argument, TypeScript automatically infers the type, making our code more concise and efficient.

Practical Applications of Generics

Developing flexible APIs with generics

We often come across situations where we need to interact with different APIs that return different types of data. To tackle this challenge effectively, TypeScript offers a handy feature called Generics. By leveraging Generics, we can create a versatile function that acts as a foundation for making API calls. This empowers us, as developers, to write code that is flexible, reusable, and seamlessly integrates with diverse API endpoints.

// Calling An API without Generics
async function ApiInstance(path: string) {
  const response = await fetch(`https://xyz.com/api${path}`)
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Let’s say if we want to call the API with this function then how we will manage the different types of different API calls data , so for that we have to use the Generics.

// Base Function
async function ApiInstance<ResultType>(path: string): Promise<ResultType> {
  const response = await fetch(`https://xyz.com/api${path}`)
  return response.json();
}

interface User {
   name: string;
   email: string;
   username: string;
}

const result = await ApiInstance<User[]>('/users');
Enter fullscreen mode Exit fullscreen mode

In the example provided above, we have transformed our base function into a generic function. This allows us to dynamically assign the type of the API response when calling the function. By utilising generics in this manner, we can effectively manage API calls and handle different response types with efficiency.

Default Type Parameters

Generics offer the flexibility to assign a default type when calling a function. This default type serves as a fallback option in case no specific type is provided. By utilising this feature, we can ensure that the function operates smoothly even when no explicit type is passed.

// Default Type Parameters
interface DefaultUser<T> {
   name: T;
}

async function ApiInstance<ResultType = DefaultUser<string>>(path: string): Promise<ResultType> {
  const response = await fetch(`https://xyz.com/api${path}`)
  return response.json();
}

const result = await ApiInstance('/users');
Enter fullscreen mode Exit fullscreen mode

Create Polymorphic Component

In libraries such as MUI and Chakra UI, you may have come across components that use the as keyword. This allows you to specify the tag name for the component, which will then be rendered as a tag you have passed in the "as" props in the Document Object Model (DOM). These components are known as polymorphic components, and they prove invaluable when developing extensive frontend applications that require consistent functionality across different components while maintaining specific themes.

Here is the basic example of it

// Polymorphic Component(Without Typescript)
const PolymorphicComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};



<PolymorphicComponent as="button">Hello Polymorphic</PolymorphicComponent>
<PolymorphicComponent as="div">Hello Polymorphic</PolymorphicComponent>
<PolymorphicComponent as="span">Hello Polymorphic</PolymorphicComponent>
<PolymorphicComponent>Hello Polymorphic</PolymorphicComponent>
Enter fullscreen mode Exit fullscreen mode

When utilising components without TypeScript, it can lead to significant errors and major two problems are here,
For instance, if we mistakenly pass an invalid tag name to the "as" prop, it may render an inappropriate element.

Additionally, if we attempt to assign properties to a specific tag, such as using the href attribute with a span tag, it would be considered incorrect.

Use of extends keyword

To address these issues, we can leverage the power of generics to overcome such challenges. By employing generics, we can ensure type safety and prevent these potential pitfalls.

export const PolymorphicComponent = <T extends React.ElementType = "span"
>({
    as,
    children,
}: {
    as?: T;
    children: React.ReactNode;
}) => {
    const Component = as || "span";

    return <Component>{children}</Component>;
};

<PolymorphicComponent as="span">Hello Polymorphic!</PolymorphicComponent> 
// It gives an error when invalid tagname is passed
<PolymorphicComponent as="macron">Hello Polymorphic!</PolymorphicComponent>
Enter fullscreen mode Exit fullscreen mode

In the given example, we can observe the usage of the extends keyword, which serves the purpose of validating the props being passed to the component. In this particular scenario, I have utilised the React.ElementType, which encompasses all the valid HTML tags. Consequently, any attempt to pass an invalid tag name will be automatically detected and prevented. However, despite this solution, we still need to address the second issue we previously discussed.

To tackle this, let's dive into the code provided below:

How to inherit with type keyword

/* React.ComponentPropsWithoutRef<"button"> -> It gives all the props of the button */
type PolymorphicComponentProps<T extends React.ElementType> = {
    as?: T;
    children: React.ReactNode;
} & React.ComponentPropsWithoutRef<T>;

export const PolymorphicComponent = <T extends React.ElementType = "span"
>({
    as,
    children,
}: PolymorphicComponentProps<T>) => {
    const Component = as || "span";

    return <Component>{children}</Component>;
};

Enter fullscreen mode Exit fullscreen mode

In the example above, I employed the type keyword to define a custom type. By incorporating the ComponentPropsWithoutRef utility, we can effortlessly access all the valid props associated with a specific HTML tag. This allows us to seamlessly inherit those props within our custom type. Consequently, if someone attempts to pass invalid props for the specified tag in the as prop, an error will be triggered, ensuring the integrity of our code. With this successful implementation, we have effectively created a powerful polymorphic component.

Conclusion

To conclude, Typescript generics offer a superior approach to writing code, enabling enhanced efficiency and the creation of small yet powerful components. By exploring the practical example provided, you have gained a comprehensive understanding of this topic and can now apply it in a practical manner within your own codebase. With a solid grasp of the fundamentals of generics in Typescript, you are well-equipped to explore more advanced concepts and further leverage the power of generics in your future projects.

Top comments (0)