Skip to content

What makes a good generic and what makes a bad one?

Daisho Komiyama edited this page Feb 23, 2024 · 2 revisions

A good generic lets type information flow through it, and you won't lose type information.

What does that mean?

We'll compare these example functions,

interface HasId {
     id: string
}

function example1<T extends HasId[]>(list:T) {
     return list.pop()
}

function example2<T extends HasId>(list:T[]) {
     return list.pop()
}

class Payment implements HasId {
     id = '0123'
}

class Invoice implements HasId {
     id = 'INVOICE_099'
}

const result1 = example1([new Payment(), new Invoice(), new Payment()])
//    ^ const result1: HasId | undefined

const result2 = example2([new Payment(), new Invoice(), new Payment()])
//    ^ const result2: Payment | Invoice | undefined

example2 is a good function declaration, example1 is not good. Why?

We're losing type information in example1, whereas example2 preserves type information.

Look at the type of return value of result1: HasId | undefined. HasId is a pretty lowest common denominator type. It's just with id property. We can't even treat these as an instance of a class, even. Whereas the type of result2 is Payment | Invoice | undefined.

The advice here is to specify your constraints in the simplest way you can.

In the example2, the argument type is explicitly given two constraints; T and []. T is extends of HasId so it's guaranteed to have id property, and [] next to the T means [T, T, T, ...].

The type of list.pop() in example1 is (method) Array<HasId>.pop(): HasId | undefined. The return type is either HasId or undefined.

But type of list.pop() defined in example2 is (method) Array<T>.pop(): T | undefined. The return type is either T or undefined. This means type T flows through the function. Remember, "A good generic lets type information flow through it".

Clone this wiki locally