Typescript Constrained Generics
Imagine you have a set of functions you'd like to use for multiple kinds of lists of objects: Product
, and Category
. At our company, we are writing some e-commerce tooling that allows admins to manage tables of data containing these two object types. The behavior of each user interface / table is very similar between objects. Thus, we want to share some functions written for performing operations on "rows" (in Typescript, Array
s) of these objects.
Using extends
to make an function sharable between many object types :
type ApiObject = {
id: string;
}
type MayHaveParent = {
parentId?: string;
}
type Product = ApiObject & { // extending the type
price: number;
categoryId: string;
}
type Category = ApiObject & MayHaveParent & { // extending the type
name: string;
}
/*
extends used here to "constrain" T to only work with ApiObject-like values, meaning they will have a string based .id
*/
const getIdsMeeting = <T extends ApiObject>(
array: T[],
filter: (element: T) => boolean
) => array.filter(filter).map((elem: T) => elem.id)
// Now ^^ Typescript can enforce this function is usable on ApiObjects only, can know that .id will exist, and not lose type information in the inner "filter" parameter when used.
const myCategoryList: Category[] = [
{ id: '1', name: 'Accessories' },
{ id: '2', name: 'Watches', parentId: '1' }
]
const hasParent = <T extends MayHaveParent>(element: T): boolean => typeof element.parentId === 'string'
const categoryIdsWithParent = getIdsMeeting(myCategoryList, hasParent)
console.log(categoryIdsWithParent)
// ['2']