Typescript Generics Golden Rule

If a type parameter only appears in one location, strongly reconsider if you actually need it.

parseYAML example

Here’s a function that parses YAML:

function parseYAML<T>(input: string): T {
  // ...
}

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:

interface Weight {  
  pounds: number;  
  ounces: number;  
}  
  
const w: Weight = parseYAML('');

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:

function parseYAML(input: string): any {  
  // ...  
}

But the recommended way to do this is by returning unknown instead:

function parseYAML(input: string): unknown {  
  // ...  
}

This will force users of the function to perform a type assertion on the result:

const w = parseYAML('') as Weight;

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!

printProperty example

How about this one?

function printProperty<T, K extends keyof T>(obj: T, key: K) {  
  console.log(obj[key]);  
}

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:

function printProperty<T>(obj: T, key: keyof T) {  
  console.log(obj[key]);  
}

This function looks superficially similar:

function getProperty<T, K extends keyof T>(obj: T, key: K) {  
  return obj[key];  
}

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:

function getProperty<T, K extends keyof T>(  
  obj: T, key: K  
): T[K]

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.

Joiner Example

What about a class?

class ClassyArray<T> {  
  arr: T[];  
  constructor(arr: T[]) { this.arr = arr; }  
  
  get(): T[] { return this.arr; }  
  add(item: T) { this.arr.push(item); }  
  remove(item: T) {  
    this.arr = this.arr.filter(el => el !== item)  
  }  
}

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:

class Joiner<T extends string | number> {  
  join(els: T[]) {  
    return els.map(el => '' + el).join(',');  
  }  
}

First of all, T only applies to join, so it can be moved down onto the method, rather than the class:

class Joiner {  
  join<T extends string | number>(els: T[]) {  
    return els.map(el => '' + el).join(',');  
  }  
}

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:

class Joiner {  
  join(els: (string|number)[]) {  
    return els.map(el => '' + el).join(',');  
  }  
}

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:

function join(els: (string|number)[]) {  
  return els.map(el => '' + el).join(',');  
}

Lengthy example

How about this function to get the length of any array-like object?

interface Lengthy {
  length:number;
}
function getLength<T extends Lengthy>(x: T){
  return x.length;
}
 

Since T only appears once after its definition, this is a bad use of generics. It could be written as:

function getLength(x: Lengthy){
  return x.length;
}
 

or even:

function getLength(x: {length:number}){
  return x.length;
}
 

Or, since TypeScript has a built-in ArrayLike type:

function getLength(x: ArrayLike){
  return x.length;
}

Reference

The Golden Rule of Generics