-
Notifications
You must be signed in to change notification settings - Fork 0
What makes a good generic and what makes a bad one?
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".