TypeScript Generics

ajcwebdev - Mar 21 '21 - - Dev Community

Outline

Introduction

I have a lot of opinions about TypeScript, which is probably bad because I don't actually know anything about TypeScript.

In an attempt to lessen my ignorance I shouted into the void that is the Lunch Dev Discord for an explanation of a particularly slippery concept, generics. I've had generics explained to me multiple times for multiple languages including Java, C#, and TypeScript but it never sticks.

Generics Dictionary Definition

Wiki's surely-correct-but-not-very-useful definition:

Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters

I then remembered that Chan told me to buy a book for this exact reason. According to TypeScript in 50 Lessons:

I don't know what I want, but I know how to get it.

Very zen.

Generics are a way to prepare types for the unknown. Whenever we know a certain behavior of a type but can't exactly say which type it affects, a generic helps us to model this behavior.

When I read that I think:

I know what I want, but I don't know what the thing I want is actually going to do, so do I really know what I want?

A Generic is Like a Function

Enter Alex Anderson, professional TypeScript explainer.

In TypeScript at least (can't speak for other language nuance), a Generic is like a function - you put a type in, and it transforms it into a different type.

Suppose you have a function:

function sum(numA, numB) {
  return numA + numB;
}
Enter fullscreen mode Exit fullscreen mode

You know that you are going to pass it two values, but those two values could be anything. They could be 1, 0, -1 or Infinity, but you don't actually know what the result is going to be until you call it.

Okay that makes sense at least, but what's the use case?

Generics Enable Reusable Types

Just like functions, the use case is making more reusable types. If you wanted to, you could inline every single function call and your program would still work. But that would be a pain to write. Same thing with Generics.

Let me grab an example from my TypeScript Course™. Very simple, almost contrived example:

function getFirstNumberItem(list: number[]): number {
  return list[0];
}
Enter fullscreen mode Exit fullscreen mode

Now rewrite that for strings:

function getFirstStringItem(list: string[]): string {
  return list[0];
}
Enter fullscreen mode Exit fullscreen mode

Notice that the function implementation is exactly the same, but we have to rewrite the entire function because the types are different. Stupid TypeScript ruining our day!😠

Okay, so I'm getting kind of an any vibe but for functions instead of types, is that in the right direction?

That's part of it; any is not type safe; You might create runtime type errors if you use any.

With Generics, you either pass it a type as an argument, to say "I would like to get the first item out this array, oh, and by the way, the array holds strings", or TypeScript infers what the generic type argument is from its usage.

Which is what happens with this generic function:

function getFirstItem<T>(list: T[]): T {
  return list[0];
}
Enter fullscreen mode Exit fullscreen mode
const stringItem = getFirstItem(["a","b","c"])
// TypeScript knows stringItem is a string
Enter fullscreen mode Exit fullscreen mode

However, you can use generics with functions, interfaces, type aliases, and classes. Let's look at another example from my course™.

Here's a type which represents a tree of strings. It allows you to create an infinitely nested set of objects with left and right properties, where each node has a string value.

type StringTree = {
  value: string;
  left?: StringTree;
  right?: StringTree;
}
Enter fullscreen mode Exit fullscreen mode

But what if you wanted a tree of numbers? Or a tree of more complicated objects? Either you create a new type definition for each (NumberTree, FruitTree, etc) or you create a generic tree type.

type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
}
Enter fullscreen mode Exit fullscreen mode

And then we can recreate our StringTree type by explicitly passing the Tree generic a String type.

Generic Constraints

A wild Ben appears!

So is the generic a way to say "It can be any type, so long as they're all consistently the same type"?

Yes. Anywhere I use the type parameter (T or whatever people use) in my type definition represents the same type. One more example from my course™.

class FruitBasket<T extends Fruit> {
  constructor(public fruits: T[] = []) {}
  add(fruit: T) {
    this.fruits.push(fruit);
  }
  eat() {
    this.fruits.pop();
  }
}
Enter fullscreen mode Exit fullscreen mode

We use T in two places:

- For the type of the array items in the fruits property.
- As the type of thing that we pass to the add method, which adds a specific fruit to our basket.

This class has a generic constraint. That's the part at the top where we say <T extends Fruit>. Fruit is a class, which means we can only use instances of that class with this FruitBasket class.

This lets us create FruitBasket instances for Apple and Banana classes that extend from Fruit, but we can't create an Onion or Lettuce FruitBasket instance, because they don't extend from Fruit.

Re generic constraints:

class Fruit {
  isFruit:true;
  constructor(public name:string) {}
}
class Apple extends Fruit {
  type:"Apple",
  constructor() {
    super("Apple")
  }
}
class Vegetable {
  isFruit: false // This makes Vegetable incompatible with Fruit
  constructor(public name: string) {}
}

const appleBasket = new FruitBasket<Apple>(); // This works

const vegetableBasket = new FruitBasket<Vegetable>();
// Type Error: Type 'Vegetable' does not satisfy the constraint 'Fruit'. Types of property 'isFruit' are incompatible.
Enter fullscreen mode Exit fullscreen mode

If this Socratic Dialogue was useful, check out Socrates's course.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .