šŸŗ TypeScript Generics explained with pets

What is a generic type?

Generic types in TypeScript give the developer more flexibility to create reusable components that can work over a different variety of types rather than a single one.

Think about generics as a type that you can specify when declaring a type, this will change what the end type looks like.

One of the most basic representation of a generic type in TypeScript is an array:

type NumArray = Array<number>;

Generic is any type that you specify inside the angle brackets <>.

How is this helpful?

Image you need a function to get the last element of an array of numbers:

const getLast = const getLast = (arr: Array<number>) => {
  return arr[arr.length - 1];
};

const lastNum = getLast([1, 2, 3]);

This will work, however how can you make this function work with strings as well?

With generic types of course!

Letā€™s refactor our implementation:

const getLast = <T>(arr: Array<T>) => {
  return arr[arr.length - 1];
};

In this case T stands for the generic type that can be passed to the function, that way we can specify the type of the input array when we call the function.

Now you can use getLast successfully with different kinds of arrays:

const lastNum = getLast([1, 2, 3]);

const lastStr = getLast(['a', 'b', 'c']);

TypeScript is smart enough to infer the type of the array so you donā€™t have to specify the generic type.

Letā€™s use our Pets example to exemplify how you could specify the generic type:

type Cat = {
  name: string;
  hasKittens: boolean;
  owner?: string;
};

type Dog = {
  name: string;
  hasPuppies: boolean;
  owner?: string;
};

const lastCat = getLast<Cat>([
  { name: 'Furry', hasKittens: false },
  { name: 'Oreo', hasKittens: true },
]);
// type Cat

const lastDog = getLast<Dog>([
  { name: 'Rambo', hasPuppies: true },
  { name: 'Rocky', hasPuppies: false },
]);
// type Dog

Now when ever you use the output of getLast you will actually have the correct type, in this case lastCat has the type of Cat and lastDog the type of Dog.

Using more than one type

You are not constraint on the amount of types to use when adding a generic. If you want to create function that creates an array of two elements but you want to specify those types as well by just including them inside the angle brackets:

const makePetArray = <A, B>(a: A, b: B): [A, B] => {
  return [a, b];
};

In this case the return type was specified as well in order to have tell TS that a tuple is the outcome of makePetArray.

Finally it can be implementing like this:

const furry: Cat = { name: 'Furry', hasKittens: false };
const rocky: Dog = { name: 'Rocky', hasPuppies: false };

const dogAndCatArr = makePetArray<Dog, Cat>(rocky, furry);
// type [Dog, Cat]

Using Default types

If you want to make the generic types optional, you can specify default values for them:

const makePetArray = <A = Dog, B = Cat>(a: A, b: B): [A, B] => {
  return [a, b];
};

Now the implementation will be simpler:

const dogAndCatArr = makePetArray(rocky, furry);

Usually the default value can be set to any if no type is specified.

Extending Generic Types

Think about the word extends as a constraint of what the type should at least include. This helps to restrict an input. Imagine if you want to add an owner to a Pet, then you want to make sure that the input is actually a pet in order to perform that operation.

In order to make this work for our Pets we will have to extend Dog or Cat to tell TS that the input of the our function has to be either a dog or a cat:

const addOwner = <T extends Dog | Cat>(pet: T, owner: string): T => {
  return {
    ...pet,
    owner,
  };
};

Then we can simple add our owners:

const ferDog = addOwner<Dog>(rocky, 'fernando');
// type Dog

const robCat = addOwner<Cat>(furry, 'Rob');
// type Cat

However if you try to add an owner to something that is not a pet, TS will get mad at you:

const teddy = { name: 'teddy' };

const mikeDog = addOwner<Dog>(teddy, 'Mike'); āŒ

// Property 'hasPuppies' is missing in type '{ name: string; }' but required in type 'Dog'

Generics in Types

You can also use generics when declaring a type. This gives you the ability to create a base type and use generics to ā€œcreateā€ different type variations. For me, this is very powerful and a great timesaver.

Letā€™s create a new Dog type as a base type:

type Dog<T> = {
  name: string;
  owner: string;
  info: T;
};

Now letā€™s add the type variations:

type GermanShepherd = Dog<'German shepherd'>;
type GoldenRetriever = Dog<'Golden retriever'>;

Finally letā€™s implement the new types:

const rambo: GermanShepherd = {
  name: 'Rambo',
  owner: 'fernando',
  info: 'German shepherd',
};

const max: GoldenRetriever = {
  name: 'Max',
  owner: 'Joe',
  info: 'Golden retriever',
};

If you try to assign something different to info , TS will tell you that is not allowed:

const fluffy: GermanShepherd = {
  name: 'Rambo',
  owner: 'fernando',
  info: 'Golden retriever' āŒ
  // Type '"Golden retriever"' is not assignable to type '"German shepherd"'
};

You do not have to create new types every time, you can also declare the types when using the Dog type:

const rambo: Dog<string> = {
  name: 'Rambo',
  owner: 'fernando',
  info: 'German shepherd',
};

const max: Dog<{ age: number; kind: string }> = {
  name: 'Max',
  owner: 'Joe',
  info: {
    age: 2,
    kind: 'Golden retriever',
  },
};

As you can see in this implementation, info can be a string or a custom object as well ā¤ļø.

Thatā€™s it for Pets and generics in TypeScript. You can access the full version of the code here.

Read more about TypeScript Utilities. Read more about Type Guards in TypeScript.

Happy hacking! šŸ‘»


fermaddev
Written by@fermaddev
I build stuff with computers and on my spare time I'm a Software Engineer.

GitHubTwitter