Intro to Generics 101

2023-05-14

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.

Image of 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