Announcing: A Free Book, A New Course, A Huge Price Cut...
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
The TypeScript docs are mostly excellent. The TypeScript handbook is a shining example of what good docs look like. They're precise, well-maintained, well laid-out, and complete.
However, if there's one section could us a rewrite, it would be the section on Generics. This section assumes too much knowledge, teaches things in the wrong order, and misses out key information. The generics section of the TypeScript docs leaves you feeling that generics are complicated and mysterious.
Let's rectify that.
Look at generics first on the type level. Imagine you have a type like this:
type Name = 'matt'
This is a literal string, matt
, expressed in a type alias. This can't be altered later, so it behaves similarly to a const
variable declaration in JavaScript:
const name = 'matt'
Using generics, we can change Name
to be a function. Add a type argument to it:
type Name<LastName> = 'matt'
This adds a parameter to Name
, meaning that whenever you use it, you'll need to pass in an argument:
type User = { name: Name<'pocock'> }
This turns the type from a variable to a function. Now, its equivalent in JavaScript would be:
const name = (lastName) => 'matt'
Since it's now a function, let's name it something more function-like:
type GetName<LastName> = 'matt'
LastName
is currently unused, so as an example return an object from our type 'function':
type GetNameObject<LastName> = {
firstName: 'matt'
lastName: LastName
}
The JavaScript equivalent would be:
const getNameObject = (lastName) => {
return {
firstName: 'matt',
lastName,
}
}
You can then use our GetNameObject
type function to create types, by passing it arguments:
type NameObject = GetNameObject<'pocock'>
// {
// firstName: "matt";
// lastName: "pocock";
// }
In Total TypeScript's Type Transformations workshop this pattern is called 'type helpers'. Type helpers are really useful on the type level to create new types to help DRY up your code.
A common example is a Maybe
type helper:
type Maybe<T> = T | null | undefined
type MaybeString = Maybe<string>
// string | null | undefined
Now you know how to handle generics on the type level, what about generic functions?
Take a simple example, returnWhatIPassIn
:
const returnWhatIPassIn = (input: any) => {
return input
}
This function won't return whatever you pass in - it'll actually always return any
on the type level. This is annoying, because it ruins your autocomplete on the thing you pass in:
const str = returnWhatIPassIn('matt')
str.touppercase() // No error here!
Imagine you wanted to create this on the type level. You'd create a type helper called ReturnWhatIPassIn
:
type ReturnWhatIPassIn<TInput> = TInput
type Str = ReturnWhatIPassIn<'matt'>
// "matt"
At the type level, you're taking in TInput
- whatever it is - and returning it.
You can use a very similar syntax to annotate our function:
const returnWhatIPassIn = <TInput>(input: TInput): TInput => {
return input
}
This is now a generic function. This means that when you call it, the type argument of TInput
gets inferred from what you pass in:
const str = returnWhatIPassIn('matt')
// "matt"
Put this in simple terms.
A generic function is a type helper layered on top of a normal function.
This means that when you call the function, you're also passing a type to the 'type helper'.
Build up that function again, piece by piece to make it clearer starting from your JavaScript-only function again:
const returnWhatIPassIn = (input: any) => {
return input
}
Get the structure of the 'type helper' set up first. You'll need to infer an TInput
argument, and return that.
const returnWhatIPassIn = <TInput>(input: any): TInput => {
return input
}
If you try and call this now, it'll infer what it returns as unknown
:
const str = returnWhatIPassIn('matt')
// unknown
You haven't told the type helper which arguments to infer from. You need it to infer the type of TInput
from the input
argument. Fix that like this:
const returnWhatIPassIn = <TInput>(input: TInput): TInput => {
return input
}
So inside (input: TInput)
, you perform a mapping between the runtime argument - input
- and the type you want it to infer - TInput
.
This is the right way to think about generics - as a type helper laid over your function, with a mapping between them.
Generics get a lot more complex from here - multiple generics, generic constraints, generics hidden deep within other types - but this mental model stays the same.
Share this article with your friends
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn why the order you specify object properties in TypeScript matters and how it can affect type inference in your functions.
Learn how to use corepack
to configure package managers in Node.js projects, ensuring you always use the correct one.
Learn how to strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
Discover when it's appropriate to use TypeScript's any
type despite its risks. Learn about legitimate cases where any
is necessary.
Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.