DEV Community

Cover image for Understanding Generics in TypeScript
Philip London
Philip London

Posted on

Understanding Generics in TypeScript

Quick note! If you'd like to experience this post interactively, go to https://codeamigo.dev/lessons/151

Introduction

Sometimes when I'm learning a new paradigm, it's the seemingly simplest things that can trip me up. I often overlook certain concepts because they seem tricky at first.

TypeScript Generics is one of those concepts.

Let's take the example below:

interface Lengthwise {
  length: number;
}

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

console.log(loggingIdentity(['hello world']))
Enter fullscreen mode Exit fullscreen mode

If you're like me you might be asking:

  1. What exactly is T here?
  2. Why is T used, is that arbitrary?
  3. Why can't I just write loggingIdentity(arg: Lengthwise)?
  4. What does mean?

What is <T>?

<T>. T tells TypeScript that this is the type that is going to be declared at run time instead of compile time. It is TypeScript's Generic Declaration.

interface Lengthwise {
  length: number;
}

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

logSomething<string>('hello world')
logSomething<Array<number>>([1])
Enter fullscreen mode Exit fullscreen mode

logSomething<string> tells TypeScript: the argument you receive will be a string, and the return type of the function will also be a string.

Why is <T> used?

Whether you use <T>, <U>, <V>, or <Type>. It's all arbitrary.

We see the use of a lot because that is how the original TypeScript documentation defined it. However, the docs have now replaced declarations using with . So It's up to you :)

How Are Generics Useful?

At this point you may be wondering, "Why should I even use Generics?"

Well let's say you wanted have a type-safe log function similar to logSomething, for both numbers and strings.

function logString(arg: string) {
  console.log(arg);
}

function logNumber(arg: number) {
  console.log(arg)
}
Enter fullscreen mode Exit fullscreen mode

Obviously we can do better, is there another approach we could use besides Generics?

Union Types vs Generics

If you were thinking about Union Types that's a pretty good idea. But it's got some limitations!

Let's say we wanted to use the return value of our function that accepts a string | number Union Type as its arg.

// function logString(arg: string) {
//   console.log(arg);
// }

// function logNumber(arg: number) {
//   console.log(arg)
// }

function returnStringOrNumber(arg: string | number) {
  return arg
}

const myVal = returnStringOrNumber(123)
const myOtherVal = returnStringOrNumber('hello')

myVal + 1 // <= Operator '+' cannot be applied to types 'string | number' and 'number'.
Enter fullscreen mode Exit fullscreen mode

Union types limit the return type of our function.

With Generics, we can tell TypeScript definitively that myVal is a number, not a string OR a number!

function returnSomething<T>(arg: T): T {
  return arg
}

const myVal = returnSomething(123)
const myOtherVal = returnSomething('hello')

myVal + 1 // 👍👍 All good!
Enter fullscreen mode Exit fullscreen mode

Overloads

Ok, well what about function overloading you may be asking.

Check out the code to the below. Sure, that works too, but I'll leave it up to you to decide which you'd rather implement.

// GENERICS
// function returnSomething<T>(arg: T): T {
//   return arg
// }

// OVERLOADING
function returnSomething(arg: number): number;
function returnSomething(arg: string): string
function returnSomething(arg: number | string) { return arg }

const myVal = returnSomething(123)
const myOtherVal = returnSomething('hello')

myVal + 1
Enter fullscreen mode Exit fullscreen mode

<T Extends...

Cool, I feel like you're starting to get it. So let's through a wrench in this whole thing.

Generics aren't perfect either. We need to understand their "constraints", by adding some constraints ;)

function getLength<T>(args: T) : number {
  return args.length;
}
Enter fullscreen mode Exit fullscreen mode

The above function will cause TypeScript to complain because we need to tell TypeScript that T extends the appropriate type and it's safe to call .length!

interface ThingWithLength {
  length: number
}

function getLength<T extends ThingWithLength>(args: T) : number {
  return args.length; // 😅 All good now!
}
Enter fullscreen mode Exit fullscreen mode

Future reading

Thanks for following along! If you enjoyed that please check https://codeamigo.dev for interactive tutorials!

Discussion (12)

Collapse
murkrage profile image
Mike Ekkel

This is a great explanation but I'm still unsure of a situation where I would use generics.

function returnStringOrNumber(arg: string | number) {
  return arg
}
Enter fullscreen mode Exit fullscreen mode

The above example can have multiple return types without the need of a generic.

function returnStringOrNumber(arg: string | number): string | number {
  return arg
}
Enter fullscreen mode Exit fullscreen mode

So I'm not sure how a generic would add to that. Granted, that's mostly due to my lack of understanding of generics in the first place 😄

Collapse
plondon profile image
Philip London Author

Hey Mike! Did you checkout the interactive tutorial? Maybe that would be helpful as well!

To answer your question, let's say we used returnStringOrNumber and assigned a variable to it.

const myStringOrNum = returnStringOrNumber(123)

Now try performing a mathematical function on myStringOrNum! TypeScript doesn't know if that's valid or not because as we said, returnStringOrNumber might return a string!

However, if we used generics:

function returnStringOrNumber<T>(arg: T): T {
  return arg
}

const myStringOrNum = returnStringOrNumber<number>(123)
Enter fullscreen mode Exit fullscreen mode

We can now perform mathematical operations on this value with safety!

Collapse
iam_danieljohns profile image
Daniel Johns

I tried this and it still ran. Is this just a JS issue or am I missing something?

function returnStringOrNumber<T>(arg: T): T {
  return arg
}
let value = returnStringOrNumber<number>(123) + "hello"
Enter fullscreen mode Exit fullscreen mode
Thread Thread
plondon profile image
Philip London Author

Hey @iam_danieljohns that's perfectly valid JavaScript, in this case value will be cast to a string type because concatenating a number and a string results in a string. In this case value will be "123hello".

If you wanted to make sure that value was indeed a number type you could do:

let value: number = returnStringOrNumber<number>(123) + "hello"
Enter fullscreen mode Exit fullscreen mode
Collapse
folken718 profile image
OldMan Montoya

Think consuming an API, you can get different types of responses, here generics are gold , because you create the function to consume once using generics and you just specify the type when you use it

Collapse
murkrage profile image
Mike Ekkel

That's a very good point :)

Collapse
captainyossarian profile image
yossarian

Most of the time you need to use generics when one argument depends on another catchts.com/infer-arguments or you need to do some validation catchts.com/type-negation , catchts.com/validators

Collapse
murkrage profile image
Mike Ekkel

Thanks for the resources!

Collapse
matttepp profile image
Matyáš Teplý

I'm learning TypeScript right now, and this was a really simple, digestible way of explaining the topic, thank you!

Collapse
bevilaquabruno profile image
Bruno Fernando Bevilaqua

Simple and objetive description abou how generic works in typescript.
Very good! I will recommend it.

Collapse
flaszer_cb6403ed69 profile image
Damien • Edited on

Good reading! I think that instead of a ThingWithLength you could simply replace that with an Array just in case someone would expect more array methods from it :)