DEV Community

Cover image for Leveraging Generics to Create Reusable Typescript Code
Kinanee Samson
Kinanee Samson

Posted on

Leveraging Generics to Create Reusable Typescript Code

Generics are a key component to creating efficient applications. Generics are an important tool that can help developers create reusable code in both JavaScript and TypeScript. Generics are a way to create functions and classes that can work with multiple types. This allows you to create code that can be used in many different contexts. By leveraging generics, you can create code that is both versatile and efficient. Generics ensures that a program is flexible as well as scalable in the long term.

Generics provide developers with a way to write code that is both dynamic and type-safe. This means that the code you write can be used with a variety of types and will still be type-safe. By using generics, developers can create code that is reusable and can be used in many different contexts. The type of generic functions is just like non-generic functions, with the type parameters listed first, similarly to function declarations. Generics in Typescript is almost similar to C# and Java generics. When we create a generic we do not declare a specific type for the argument, generics alienate the need to explicitly declare the type of a function argument or return payload. You will find the common pattern. function<T>(arg: T): T{} however generics are not limited to only functions, you can use generics with classes, Object literals, interfaces and type definitions. Let's look at generic function definition.

function generic<T>(param: T): T {
 return param;
}

const gen = generic(2);
const newGen = generic("sam");
console.log(typeof gen);
console.log(typeof newGen);
// number
// string
Enter fullscreen mode Exit fullscreen mode

When we make use of the Generics we allow typescript to infer a type based on the value we pass in. When we consumed the generic we did not pass in a type argument. It changes nothing and this behavior allows for a convenient way to match the type for any given value.

We can narrow our generics to only handle a set number of type, this allows our code to be flexible up to a given extent.

function generic<T extends string | number>(arg: T): T{
    if (typeof arg == "string"){
        return arg.toUpperCase()
    } return Math.pow(arg, 2);
}

generic("sam");
generic(2);
generic({name: "foo"}) // not possible
Enter fullscreen mode Exit fullscreen mode

Since we narrowed the type to string or number, this function can only process numbers or strings. An argument of any other type will not be accepted by the Typescript compiler. This knowledge can be built upon to solve a particle problem in Typescript. When use Object.keys() on an object, there is no way to infer the type tuple we get back. If you try to use a key in the array to access the resulting property on the object we get an error No index signature with a parameter of type 'string' was found on type.

const Obj  = {
  title: "User",
  fields: {
    name: {
      type: String
    },
    age: {
      type: Number
    },
    book: {
      type: Book
    }
  }
}

const keys = Object.keys(Obj)
Obj[keys[1]]
// No index signature with a parameter of type 'string' was found on type
Enter fullscreen mode Exit fullscreen mode

We can work around this problem using a generic function.

const objKeys = function<T extends Object>(obj: T): (keyof T)[]{
 return Object.keys(obj) as (keyof T)[]
}

const keys = objKeys(Obj.fields);
Obj[keys[1]] // is valid
Enter fullscreen mode Exit fullscreen mode

This code is defining a generic type function called objKeys. The function takes an object as an argument and returns a tuple of keys that match the same type as the object's keys. The type of the array returned is keyof T which is the union of all the keys in obj. This allows the user to access the type of the keys in the object.

Generics can also be used with an interface, we can define a generic interface. Say we have a user document and a field that stores user privileges , privileges may be an array of strings, or an array of numbers or an array of object.


type PRIVILEGES = string[] | number[] | Object[]
interface IDocument<T> {
    type: string
    privileges: T
}

function makeUser(name: string, privileges: PRIVILEGES): IDocument<PRIVILEGES> {
    return  { type: "ADMIN", privileges }
}
Enter fullscreen mode Exit fullscreen mode

This can also be used with classes too if that's the approach we prefer.

type PRIVILEGES = string[] | number[] | Object[]
interface IDocument<T> {
    type: string
    privileges: T
}

class Documents implements IDocument<PRIVILEGES>{
    constructor(
public type: string, 
public privileges: PRIVILEGES){}
}
Enter fullscreen mode Exit fullscreen mode

Generics also make it easier to debug code. Since generics provide type-safety, it is easier to identify bugs and errors in the code. This makes debugging much easier and faster.

Overall, generics are an important tool for creating reusable code in both JavaScript and Typescript. By leveraging the power of generics, developers can create versatile and efficient code that can be used in many different contexts. Generics also make it easier to debug code and identify bugs and errors. We can combine Generics with type guards to produce some interesting patterns.

Oldest comments (0)