Generics in Typescript allows you to create a component that can work with different types. This lets users use these components with their specific type.
Before reading this article, please read my article about TS utility types as It will help you to understand this article better.
Table of contents
- 1. Generics with type
- 2. Generic functions
- 3. Generics with built-in functions
- 4. Inferring the types
- 5. Constraints on type arguments
- 6. Constraints in functions
- 7. Use
as
when necessary - 8. Multiple generics
- 9. Defaults in type arguments
- 10. Class Types in Generics
- Conclusion
Here are few possible ways to use
Generics
in Typescript:
1. Generics with type
The easiest way of using generics
is with type.
For example:
type MyData<Type> = {
data: Type
}
type DataAsString = MyData<string>;
// type DataAsString = {
// data: string;
// }
Because we passed string
as a type parameter, our data will be string
type. Likewise, if we pass the number
type, our data will be of number type. And that's the beauty of generics.
You can work with any type.
type MyData<Type> = {
data: Type
}
type DataAsString = MyData<number>;
// type DataAsString = {
// data: number;
// }
2. Generic functions
As you can pass arguments to a function, you can also pass type arguments
to a function. For example:
const fetchData = <Type>(url: string): Promise<Type> => {
return fetch(url)
}
fetchData<{name: string, age: number}>("/api/books")
.then(res => {
console.log(res)
// res: {name: string, age: number}
})
We passed {name: string, age: number}
as a type argument
to the fetchData
function, And this type argument tells res
what It is supposed to be because this is what's getting typed in Promise<Type>.
So now the function fetchData
has become a generic function. A generic function is just a normal function but includes a type argument.
If we want to pass another type, our res
will replicate that same type.
const fetchData = <Type>(url: string): Promise<Type> => {
return fetch(url)
}
fetchData<{id: number, email: string}>("/api/books")
.then(res => {
console.log(res)
// res: {id: number, email: string}
})
3. Generics with built-in functions
You can even use generics with built-in functions. For example, you can create a Set
that stores only numbers by using the Set constructor with a type parameter:
const numberSet = new Set<number>();
numberSet.add(1);
numberSet.add(2);
numberSet.add(3);
Similarly, you can create a Map
that maps strings to numbers by using the Map constructor with type parameters for the key and value types:
const stringToNumberMap = new Map<string, number>();
stringToNumberMap.set("one", 1);
stringToNumberMap.set("two", 2);
stringToNumberMap.set("three", 3);
4. Inferring the types
If your type argument
looks similar to your runtime argument
then you don't have to pass a generic type
to your function. For example:
function identity<T>(arg: T): T {
return arg;
}
const result1 = identity<number>(42); // const result1: number
const result2 = identity<string>("hello"); // const result2: string
While the above example looks correct, we can simplify it more:
function identity<T>(arg: T): T {
return arg;
}
const result1 = identity(42); // const result1: number
const result2 = identity("hello"); // const result2: string
In this case, because we are not passing any type argument
, then Typescript
will look in the runtime argument
to see If It can infer
anything from it.
So the return type is inferred
from the runtime argument type, and the function returns the same type as it receives.
5. Constraints on type arguments
In our following example, we wanted to be able to use the ReturnType
utility type, but the compiler could not prove that every type was a function; it could be a string or number, and as we know, that ReturnType
works only with functions, so it warns us that we can’t make this assumption.
type GetPromiseData<T> = Awaited<ReturnType<T>>
// Error: Type 'T' does not satisfy the constraint '(...args: any) => any'
type PromiseResult = GetPromiseData<
() => Promise<{
id: string
email: string
}>
>
So to make It clear to Tyepscript, We have to say that T
is only a function, and we can do this by adding extends (...args: any) => any
after the T
:
type GetPromiseData<T extends (...args: any) => any> = Awaited<ReturnType<T>>
type PromiseResult = GetPromiseData<
() => Promise<{
id: string
email: string
}>
>
6. Constraints in functions
In our following example, we wanted to access the .address
property of arg, but the compiler could not prove that every type has a .address
property, so it warned us that we couldn’t make this assumption.
function myFunc<Type>(arg: Type): Type {
console.log(arg.address); // Property 'address' does not exist on type 'Type'.
return arg;
}
To fix this error, we can create an interface with address
property in it and extend it with the Type
argument:
interface IDetails {
address: string;
}
function myFunc<Type extends IDetails>(arg: Type): Type {
console.log(arg.address); // Now we know it has a .address property, so no more error
return arg;
}
Because the generic function is now constrained, it will no longer work over all types:
myFunc(true);
// error: Argument of type 'boolean' is not assignable to parameter of type 'IDetails'.
Instead, we need to pass in values whose type has address
property:
myFunc({ length: 10, address: "Something" });
7. Use as
when necessary
Sometimes using as
is the best thing you can do when using generics. For example:
const ObjectKeys = <T extends {}>(obj: T): Array<keyof T> => {
return Object.keys(obj) // Error: Type 'string[]' is not assignable to type '(keyof T)[]'.
}
const result = ObjectKeys({
id: 6,
email: "me@gmail.com"
})
Here we are getting an error because the Object.keys
method returns an array of string values, and TypeScript cannot guarantee that the string values returned by Object.keys
actually correspond to valid keys of the generic type T.
To fix this error, we can explicitly typecast the Object.keys
result to an array of keyof T
using the as
keyword:
const ObjectKeys = <T extends {}>(obj: T) => {
return Object.keys(obj) as Array<keyof T>
}
const result = ObjectKeys({
id: 6,
email: "me@gmail.com"
})
8. Multiple generics
Sometimes we have to use multiple generics to make sure that we are getting back a certain type. Consider the following example:
function getProperty<T>(obj: T, key: keyof T) {
return obj[key];
}
let x = { a: 1, b: "b", c: true, d: 4 };
getProperty(x, "a"); // return type: string | number | boolean
While the above example is correct, the problem is that the return type is not explicit. Return type is an union
consisting of 3 different types. To fix this problem, we can use multiple generic types:
function getProperty<T, Key extends keyof T>(obj: T, key: Key) {
return obj[key];
}
let x = { a: 1, b: "b", c: true, d: 4 };
getProperty(x, "a"); // return type: number
In this example, we've added generic type parameter Key
to represent the key type of the object T
. So now we will get a specific return type only.
function getProperty<T, Key extends keyof T>(obj: T, key: Key) {
return obj[key];
}
let x = { a: 1, b: "b", c: true, d: 4 };
getProperty(x, "c"); // return type: boolean
9. Defaults in type arguments
We can also use default types in generics. Consider the following example:
const makeSet = <T>() => {
return new Set<T>()
}
const mySet = makeSet() // const mySet: Set<unknown>
Here we are getting unknown
because we didn't pass any type argument to makeSet
function. We can solve this problem by either passing a type argument like this, makeSet<number>()
or by specifying a default type:
const makeSet = <T = number>() => {
return new Set<T>()
}
const mySet = makeSet() // const mySet: Set<number>
10. Class Types in Generics
We can also refer to class types by their constructor functions. For example:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
Here's a breakdown of how the function works:
The
create
function is declared with a type parameterType
, representing the type the function will make.The function takes a single argument
c
, an object representing a constructor function for the typeType
. The argument has the type{ new (): Type }
, an object type specifying a constructor function that takes no arguments and returns a value of typeType
.The
new
keyword is used to create a new instance of the typeType
inside the function by calling the constructor function passed as the argumentc
. Thenew
keyword creates a further object of the typeType
and returns it as the result of the function.The function's return type is specified as
Type
, which ensures that the function returns an instance of the type specified by the type parameter.
Here's an example of how the create
function can be used:
class MyClass {
constructor(public value: string) {}
}
const instance = create(MyClass);
console.log(instance.value); // Output: undefined
This example defines a simple class, MyClass,
with a single property value. We then call the create
function, passing the MyClass
constructor function as the argument. The create
function creates a new instance of MyClass
using the constructor function and returns it as an instance of type MyClass.
Conclusion
In conclusion, TypeScript's support for generics provides a powerful tool for writing type-safe and reusable code. By defining generic types and functions, you can create code that works with various types while maintaining strict type-checking.
To make the best use of generics in TypeScript, it's essential to understand how to define and use generic types, specify constraints on generic type parameters, and use type inference to reduce the need for explicit type annotations. Additionally, it's crucial to use generics to maintain good code readability and avoid unnecessary complexity.
When appropriately used, generics can significantly improve the quality and maintainability of your TypeScript code. By taking advantage of TypeScript's powerful type system and the flexibility of generic types, you can create highly reusable and expressive code while still maintaining the strong type safety that TypeScript provides.
Visit:
👨💻My Portfolio
🏞️My Fiverr
🌉My Github
🧙♂️My LinkedIn
Top comments (2)
Great article!
I was wondering if you can help me with a question.
Is it possible to define a generic class where Type must extend a generic interface?
I know that the code bellow doesn't work, but it's something like this:
class MyClass<T extends MyGenericInterface<K, Z>> {
Thanks in advance!
Sorry sir, I tried many ways to find the solution. Unfortunately could not find it. I will keep looking for the solution. In the meantime, thank you so much for finding the article helpful😊