Overview
Within the realms of Object-Oriented Programming, generic types are a very common and useful way to build components which can work with a variety of types instead of only one. Fortunately for us, we can do the same generic types in functional programming using TypeScript Generics.
Using generic parameters
- Simple example
A simple generic parameter in a function looks like this:
function myFunc<T>(arg: T) {
return value;
}
A generic type can be defined by using <GenericAnnotation>
after the name of our function. Then simply specify the type of the argument to be the generic type. The return value of a function can also be defined as a generic type, e.g. myFunc<T>(arg: any): T
- More Interesting Example
Let's have the following interface IUserDetails
which specifies different user properties:
interface IUserDetails {
firstName: string;
lastName: string;
age: number;
dob: Date;
}
For the sake of this example, let's imagine that each user property needs to be updated separately. One way to do this is by writing a function for each property which can be strongly typed independently for each property.
Instead of doing that, let's build a generic function which will allow to pass any of the user properties and their correct types. Here is how that might look like:
function updateUserDetails<
TKey extends keyof IUserDetails,
TValue extends IUserDetails[TKey]
>(key: TKey, value: TValue) {
// Update user details
}
Pretty cool isn't it? Let's see what the generic type constraints are doing.
There are two generics in the function, one for the key of the user property, TKey
, and the other for the value of it, TValue
.
-
TKey
has a defined type constaint, specifying that it can only be one of the keys ofIUserDetails
interface. -
TValue
has a defined type constraint, specifying that the type can only be the type of the defined user property.
Calling the function like this: updateUserDetails("dob", "Jack")
will throw a type error as the dob
property of the interface expects a type of Date, but executing the function like updateUserDetails("firstName", "Jack")
will work as it's passing the correct type forfirstName
property.
Using Generic types when building React components
Typescript Generics can also be applied when building React components.
Here is an example. Let's build a List which can receive any types of list items with predefined generic constraints.
This is the bare minimum a List item object needs to have:
interface IBaseListItem {
id: string;
name: string;
}
The props of the list will look like this:
interface IListProps<T extends IBaseListItem> {
title: string;
items: T[];
}
The items[]
property is defined as a generic type with a constaint which provides the bare minimum the type needs to have in order to be accepted, in this case is IBaseListItem
.
The component definition can look like this:
function List<T extends IBaseListItem>(props: IListProps<T>) {
return (
<ul>
{props.items.map((it) => (
<li key={it.id}>{it.name}</li>
))}
</ul>
);
}
The react component has defined generic type which is provided to the generic props IListProps
. As the generic type of the function has the same generic constraint as the IListProps
, the generic type will be accepted.
Top comments (1)
Nice! Had a brief moment where I was worried that using the generic would create a parser error, since it's natural to assume it'd look like:
<List<number> />
. Then I remembered that, unlike C# (or a lot of languages with generics), TypeScript infers the type parameter, which means the usage would look the same as a non-generic component. Very clever!