A major part of software engineering is building components that not only have well-defined and consistent APIs but are also reusable. Generics in Typescript allows the creation of components that will work with multiple types.
The most basic example of using generics is an array. With Typescript, you can define the types that will fill the array:
const strArr: string[] = ["one", "two", "three"];
strArr.push(1) // This would error
If we were to use string[]
to define an arg type in a function, as expected, we would get errors if the arg was not an array of strings. However, there are scenarios where a function may be required that will be able to handle arrays of different types. Consider the following example - getLast
returns the last item in an array
const getLast = (arr: string[]): string => {
return arr[arr.index - 1];
}
// 'test2'
console.log(getLast(['test', 'test2']);
// Will error
console.log(getLast([1,2,3]);
Here you can see, we are restricting to the array we can pass into getLast
to only contain strings. However, it would be perfectly reasonable to want to return the last item in an array containing any types. Here, we can use a type variable, a special variable that works on types rather than values:
const getLast = <T>(arr: T[]): T => {
return arr[arr.length - 1];
}
As you can see above, we can define the type of array being passed into getLast
as the type variable T
(note - T
is the conventional name for a type variable, however, any name is usable), and Typescript will also be aware of the return type. This turns the above into a generic function. To call the function with different types, the syntax is the following:
const numOutput = getLast<number[]>([1,2,3]) // output will be of type 'number'(3)
const strOutput = getLast<string[]>(['a','b','c']) // output will be of type 'string'('c')
It is possible to use more than one type variable when creating a generic function:
cont makeArr = <X, Y>(x: X, y: Y): [X, Y] => {
return [x, y];
}
const v = makeArr<number, string>([1,'a']); // returns type ['number', 'string']
// Types can be inferred by Typescript, <number, number> is not necessary.
const v2 = makeArr(2,3) // returns type ['number', 'number']
It is possible to extend the type variable from other object types. If we wanted to make a function that only works on types that have a .length
property. To do so, we must list our requirement as a constraint on what T
can be.
To do so, we’ll create an interface that describes our constraint. Here, we’ll create an interface that has a single .length
property and then we’ll use this interface and the extends
keyword to denote our constraint:
interface WithLength {
length: number
}
function echo<T extends WithLength>(arg: T): T {
console.log(arg.length); // arg has a .length property, so no error
return arg;
}
Generics with Interfaces
It is possible to use generics to create more flexible interfaces in our code. The syntax is similar to the above examples. This is useful when we can only be sure of part of the shape of an object, such as an HTTP request:
interface HttpResponse<T = any> {
success: boolean
error?: string
data: T
}
type StringResponse = HttpResponse<string>
The above could then be used in a function to allow more control over the returned data in our code base.
interface Person {
name: string
age: number
}
const makeRequest = async<T = any>(): Promise<HttpResponse<T>> => {
const response = await // some logic
return response;
}
const data = makeRequest<Person>() // resolves with data as a Person
Top comments (0)