Generics are often taught as a way to avoid repeating types. That is true, but the deeper value is preserving a relationship. A function that returns the first item should return the same type it received, not a widened unknown.
function first<T>(items: T[]): T | undefined {
return items[0]
}
function byId<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id)
}
const named = byId([{ id: 'html', title: 'HTML' }], 'html')
// named?.title is available without a castThe two constraints worth learning first
- Use a constraint when the function needs a capability: T extends { id: string } says the value has an id.
- Use keyof when the function needs a property name: K extends keyof T.
- If a generic appears only once and has no constraint, a concrete type is probably clearer.
Do not start with conditional types and recursive inference. Start with a function you would otherwise write twice. The compiler will tell you when the relationship is not actually preserved.
Corrections and technical review are welcome. Tell us what we missed.