Generics are parameters for Types. Or close enough to be able to use as an initial mental model.
To get started, declare a type that accepts a parameter < Generic >
type User<IdType> = { id:IdType }
This let's you pass in a parameter to the type
const johnny: User<string> = { id:'uuid123' }
A scenario where this could be used is if you have users with an id field that could be either a string or a number. Generics can then be used to specify which user needs id to be string and which needs id to be number
type User<IdType extends string | number> = { // extends string | number means IdType // has to be a string or number name: string; id:IdType } const numberUser: User<number> = { name: 'Johnny', id: 5, } // type is evaluated as // type User { // name: string; // id: number //} const uuidUser:User<string> = { name: 'Johnny', id: 'uuid-string-123', } // type is evaluated as // type User { // name: string; // id: string //}
Let's move on to making a Map exclusive to dogs.
Here's a scenario with different animals where we want to create a Map with only dogs. First we define the AnimalTypes and then we declare an Animal
type that accepts an AnimalType
type AnimalTypes = 'dog' | 'cat' | 'parrot' type Animal<AnimalType extends AnimalTypes> = { // Only AnimalTypes allowed in the // AnimalType parameter name: string, type: AnimalType, age: number, }
Next we create a dogMap
where the key has to be a number and the value has to be an animal where the type is 'dog'. We specify this through the type parameters that we put in the generics
const dogMap = new Map<number, Animal<'dog'>>()
Here's some code showing how dogMap works
const fido: Animal<'dog'> = { name: "Fido", type: 'dog', age: 4, } const doug: Animal<'cat'> = { name: "Doug", type: 'cat', age: 1, } dogMap.set(1, fido) // fido allowed since he's a dog dogMap.set(2, doug) // not allowed , only dogs allowed
This code can be simplified by creating new types from the generic one
type AnimalTypes = 'dog' | 'cat' | 'parrot' type Animal<AnimalType> = { name: string, type: AnimalType, age: number, } type Dog = Animal<'dog'> type Cat = Animal<'cat'> const dogMap = new Map<number, Dog>() const fido: Dog = { name: "Fido", type: 'dog', age: 4, } const doug: Cat = { name: "Doug", type: 'cat', age: 1, } dogMap.set(1, fido) dogMap.set(2, doug) // still no cats allowed
Let's move on from dogs and Maps to functions.
Here we want to be able to pass in a variable and add a uuid to it and return it.
const addUuid = <ItemType>(item: ItemType) => { return { ...item, uuid: "uuid-1234" } } const uuidUser = addUuid({ name: 'Johnny' }) // { name: 'Johnny', uuid: 'uuid-1234' } // const uuidUser: { // name: string; // } & { // uuid: string; // }
A generic is better than any
because it returns a type instead of "any" and with that we also get intellisense.
We didn't need to declare our generic this time. Typescript could infer it from the item variable.
When we write item:ItemType
ItemType will be whatever is passed in as item
Now that's all there is to a 101 introduction.
If you want to dig deeper I recommend the ts documentation , no bs ts or playing around in the typescript playground