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;
}
You know that you are going to pass it two values, but those two values could be anything. They could be
1
,0
,-1
orInfinity
, 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];
}
Now rewrite that for strings:
function getFirstStringItem(list: string[]): string {
return list[0];
}
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 useany
.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];
}
const stringItem = getFirstItem(["a","b","c"])
// TypeScript knows stringItem is a string
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
andright
properties, where each node has a string value.
type StringTree = {
value: string;
left?: StringTree;
right?: StringTree;
}
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>;
}
And then we can
recreate
our StringTree type by explicitly passing theTree
generic aString
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();
}
}
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 theadd
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 thisFruitBasket
class.This lets us create
FruitBasket
instances forApple
andBanana
classes that extend fromFruit
, but we can't create anOnion
orLettuce
FruitBasket
instance, because they don't extend fromFruit
.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.
If this Socratic Dialogue was useful, check out Socrates's course.