The New TypeScript Handbook has some real gems in it. Here's what it has to say about generics:
Writing generic functions is fun, and it can be easy to get carried away with type parameters. Having too many type parameters or using constraints where they aren't needed can make inference less successful, frustrating callers of your function.
It goes on to offer a few specific pieces of advice about how to use generics, including one that I've started to think of as the "Golden Rule of Generics":
Type Parameters Should Appear Twice
Type parameters are for relating the types of multiple values. If a type parameter is only used once in the function signature, it's not relating anything.
Rule: If a type parameter only appears in one location, strongly reconsider if you actually need it.
I love this rule because it's so concrete. It gives you a specific way to tell whether any use of generics is good or bad.
But it's not always obvious how to apply this rule, and it doesn't offer much guidance about how rework your code if you're using generics poorly. So in a nod to the folks at 538, let's play the "good use of generics or bad use of generics" game. The examples that follow have been edited to protect the innocent, but they are all based on real code I've encountered either in blog posts or in print.
Let's start with the identity function:
|
Good use of generics or bad use? In this example the generic argument, T
, appears in two places after its declaration:
|
So this passes the test and is a good use of generics. And rightly so: it relates two types because it says that the input parameter's type and the return type are the same.
How about this one?
|
The generic parameter C
appears twice, so it's fine. But A
and B
only appear once (other than in their declarations), so this function fails the test. In fact, we can rewrite it using only one generic parameter:
|
Here's a function that parses YAML:
|
Is this a good use of generics or a bad use of generics? The type parameter T
only appears once, so it must be bad. How to fix it? It depends what your goal is. These so-called "return-only generics" are dangerous because they're equivalent to any
, but don't use the word any
:
|
The Weight
here could be any type and this code would type check. If that's what you want, you may as well just be explicit about your any
:
|
But the recommended way to do this is by returning unknown
instead:
|
This will force users of the function to perform a type assertion on the result:
|
This is actually a good thing since it forces you to be explicit about your unsafe type assertion. There are no illusions of type safety here!
How about this one?
|
Since K
only appears once, this is a bad use of generics (T
appears both as a parameter type and as a constraint on K
). Fix it by moving the keyof T
into the parameter type and eliminating K
:
|
This function looks superficially similar:
|
This one, however, is actually a good use of generics! The trick is to look at the return type of this function. Hovering over it in your editor, you can see its full type:
|
The return type is inferred as T[K]
, so K
does appear twice! This is a good use of generics: K
is related to T
, and the return type is related to both K
and T
.
What about a class?
|
This is fine since T
appears many times in the implementation (I count 5). When you instantiate a ClassyArray
, you bind the type variable and it relates the types of all the properties and methods on the class.
This class, on the other hand, fails the test:
|
First of all, T
only applies to join
, so it can be moved down onto the method, rather than the class:
|
By moving the declaration of T
closer to its use, we make it possible for TypeScript to infer the type of T
. Generally this is what you want!
But in this case, since T
only appears once, you should make it non-generic:
|
Finally, why does this need to be a class at all? This noun-ing feels like a Java-ism. Just make it a standalone function:
|
How about this function to get the length of any array-like object?
|
Since T
only appears once after its definition, this is a bad use of generics. It could be written as:
|
or even:
|
Or, since TypeScript has a built-in ArrayLike
type:
|
If you've made it this far, you should have a good sense for how to apply the golden rule of generics and how to fix the declarations that break it. As you read and write generic functions, think about whether they follow this rule! If you're having trouble telling or aren't sure how to fix one, tweet at me!
(Credit for this rule goes to TypeScript engineering lead Ryan Cavanaugh. If you like it, be sure to thank him! If you really like it, consider turning it into an eslint rule. Golden ruler is from FreeSvg.)