How Typescript Generic Types Work
📣 Sponsor
Typescript generics are a way to take a function that that has an argument which we can define when we call that argument - the same way we can change an argument of a function when we call it.
If you're new to the concept of Typescript generics, then read on to learn how they work.
How Typescript Generics Work
Imagine a function like this the one below - it has an argument which is of type string, and it outputs a string on top of that:
let myFunction = function(arg: string):string {
return `Hello, the value of your argument is ${arg}`;
}
// This will return "Hello, the value of your argument is World"
myFunction("World");
Interestingly, myFunction
works both with a variety of types, such as a string
, or number
. When we make something work over a multitude of different circumstances, it's called a generic, and that's exactly what a Typescript generic is.
To adjust our Typescript function to become a generic, we add a definable argument in <>
s straight after the function keyword:
let myFunction = function<NewType>(arg: NewType):string {
return `Hello, the value of your argument is ${arg}`;
}
// This will return "Hello, the value of your argument is World"
myFunction<number>(5);
Think of generic parameters in Typescript the same way as arguments in vanilla Javascript. In the example above, we've made a new argument, called NewType
- but you can call it anything. Our argument, arg
, is of type NewType
.
Then, when we call myFunction()
, we can define the type of our argument - in the case above, a number.
Generics with multiple types
Imagine another example, where we aren't using string literals - in the example below, we simply add our two arguments together:
let myFunction = function(x: string, y: string):string {
return x + y;
}
// Returns HelloWorld
myFunction("Hello", "World");
Again, this works with multiple types - most notably strings and numbers. Let's try adding a generic type again:
let myFunction = function<NewType>(x: NewType, y: NewType):number {
return x.length + y.length;
}
Except, this time, we'll get an error:
Property 'length' does not exist on type 'NewType'
The reason this throws an error, is because we haven't defined what NewType
can and can't do. To resolve this example, we simply have to mention that x and y will be an Array of NewType
s by using []
brackets:
let myFunction = function<NewType>(x: NewType[], y: NewType[]):number {
return x.length + y.length;
}
// This will return 6 (3 + 3)
myFunction<number>([ 5, 6, 7 ], [ 10, 9, 8 ]);
Extending Generic Types
Sometimes, however, we have a different set of circumstances. We may have a situation where we want to extend NewType
, for example, if we are accessing a property that NewType
should have, but the compiler does not know about. We can extend generic types using the extends
keyword. That means we can constrain any types passed to our function, so they have a minimum set of properties.
In the below example, all elements of type NewType
should have at least the property name
:
type ExtendedNewType = {
name: string
}
type User = {
name: string,
age?: number
}
// NewType must contain at least the attribute "name" - so lets add it as an extension of our ExtendedNewType type.
let myFunction = function<NewType extends ExtendedNewType>(x: NewType, y: NewType):string {
return `${x.name} ${y.name}`
}
// This will return "Hello World"
let runFunction = myFunction<User>({ name: "Hello" }, { name: "World" });
Custom Types
We are defining custom types above. If you are new to custom types, try reading our guide on creating custom types in Typescript
Generic Custom Types
As well as applying generic types to our functions, we can also apply them to our own custom types. In the below example, we have a user where an ID may be a string or a number. We can define a generic type at the top level of our new type, and then define it whenever we use the User
type.
// Type User can have an ID which is either a number or a string
type User<CustomType extends (number | string)> = {
id: CustomType,
name?: string,
age?: number
}
// In this case, we define CustomType as a string
let myUser:User<string> = {
id: "1234-1234-1234",
name: "John Doe",
age: 24
}
// In this case, we define CustomType as a number
let myOtherUser:User<number> = {
id: 1234,
name: "Jane Seymore",
age: 48
}
More Tips and Tricks for Typescript
- How the TypeScript ReturnType Type works
- How the TypeScript Record Type Works
- How the TypeScript Extract Type Works
- How Typescript Enums Work
- How the TypeScript Exclude Type Works
- How the typeof Operator works in TypeScript
- How the TypeScript Parameters Type Works
- Typescript Tuples, and how they work
- The Difference between TypeScript Interfaces and Types
- How the TypeScript Required Type Works