DEV Community

shadow1349
shadow1349

Posted on • Edited on

TypeScript tip of the week - generics

Happy Tuesday and thanks for reading the first ever TypeScript tip of the week. I'm starting this series helpful TypeScript insights and tips as a way to get more people interested in using TypeScript.

This week I wanted to talk about TypeScript generics. With an incredible type system backing TypeScript, generics give you the power to easily reuse logic and give you the ability to dynamically define what type of data you're expecting back from that logic. Let's look at an example.

function foo<T>(arg: T): T {
    return arg;
}


const bar = foo<string>('baz');

This is a super basic example, but what happens if we want to perform an operation on this?

function foo<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}

There are many types that have a .length attribute, but if we try to use this new code, we'll get an error because there are some types that do not have that property. To solve this problem we can extend these types.

function foo<T extends string>(arg: T): T {
    console.log(arg.length);
    return arg;
}

This will work, however, it kind of defeats the purpose a little because T could be an array or an object with a length property on it. Using a custom interface, we can zero in on the property that we need to extend with our generic type.

interface GenericLengthType {
    length: number;
}

function foo<T extends GenericLengthType>(arg: T): T {
   console.log(arg.length);
   return arg;
}

const a = foo(['foo', 'bar', 'baz']);

const b = foo('bar');

const c = foo({length: 10});

const d = foo(10); // error number doesn't have property length

This is all well and good, but let's look at a more practical example, one I use often when writing TypeScript code.

function TryWrapper<T extends Function>(fn: T): T {
  return <any>function() {
    try {
      return fn(...arguments);
    } catch (e) {
      // handle error gracefully
    }
  };
}

const test = TryWrapper((arg: string) => {
  if (arg.length < 5) {
    throw new Error("Argument length must be longer than 5");
  }
  return arg.split("");
});

It may seem odd that we're using type any on the returned function, however, if we don't put anything there we will get this error

Type '(args: any) => any' is not assignable to type 'T'.
'(args: any) => any' is assignable to the constraint of type 'T',
but 'T' could be instantiated with a different subtype of constraint 'Function'.

When we leave out that any it's basically trying to assign that type to T, which it's telling us is invalid. We also use the arguments keyword to grab all the arguments passed into the function. We then use the spread operator to make sure that the arguments are passed into the wrapped function. The reason we do this is that a wrapped function may have several arguments passed into it and we want to make sure to preserve them.

Here is a live example on stackblitz

Thanks for reading the first TypeScript tip of the week! Tell me what you think in the comments.

Top comments (1)

Collapse
 
zanehannanau profile image
ZaneHannanAU

Here's one I use myself, which is super magical:

/** Moves the this parameter out of the called function */
//@ts-ignore
export const fbind = <F extends (this: T, ...args: any[]) => R, T, R>(fn: F): ((self: T, ...args: Parameters<F>) => R) => Function["prototype"]["call"]["bind"](fn)

(Note: the square brackets are for --mangle-props strict mode in terser)

this is a little better and acts only as a call function, but requires a reference so it's only useful on native callers like Array.prototype.push or MessagePort.prototype.postMessage, and makes it more of a take-by-reference function in rust. Modular functional programming if you will.