Generic Interfaces and Functions in Typescript
Generic Interfaces and Functions in Typescript

In this video you will learn what are generics in Typescript and how they are working. So let's jump right into it.

So here I have already setted up empty project for writing typescript. If you missed my video where we installed all needed packages and configured our project I will link it in the description.

So normally people don't learn generics and just jump in Typescript because it's really similar to Typescript. Then they are opening a definitions of some function, don't understand anything and close it forever.

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ramda/index.d.ts#L295

But this is a powerful tool if you learn it starting on simple examples. Let's check this out.

const addId = (obj) => {
  const id = Math.random().toString(16);
  return {
    ...obj,
    id,
  };
};

const user = {
  name: "Jack",
};

const result = addId(user);

So here we have an add function which writes id in the object that we passed inside. The is working for sure but we can't really know what object we are passing inside. This is why Typescript thinks that here is any. But we actually said that any is bad. Exactly this problem generics are trying to solve. When we don't know inside what will be the type we can pass it from outside and use without knowing inside our function.

Here is how it looks like.

const addId = <T>(obj: T) => {

So we put but letter T in tags before arguments and we can then use this T as a type inside the whole function. In arguments or in returned value or anywhere. So you can think of it as a way to pass types inside a function as arguments.

So here actually a lot of magic happened. If we check the types you can see that now Typescript understands that from outside we got some type. and used it and returned back with id property. This is why in result we have an object and ID.


We can also specify from outside the type by ourselves if we want. If you don't know what interfaces are go check my video about them first.

interface User {
  name: string;
}

const user: User = {
  name: "Jack",
};

but we also can say explicitly what type we are passing.

const result = addId<User>(user);

This is really handy if Typescript can't understand it or we want to make it readable on the first glance.


But here is a problem. We can now provide whatever type we want here. For example string which won't be correct at all because we are working with object inside.

const result2 = addId<string>("foo");

So how we can avoid passing completely wrong type? For this we have extends. We can specify that whatever type we want to pass inside it should be extended from object.

const addId = <T extends object>(obj: T) => {

With such notation we can't pass inside a string or provide a string type for example because it's not extends from object.

const result2 = addId("foo");
const result2 = addId<string>("foo");

We can also in the same way as with functions use generics with interfaces. It can be possible that we don't know all types in the interface and we might want to pass them from outside.

interface User<T> {
  name: string;
  data: T;
}

const user: User = {
  name: "Jack",
  data: {
    meta: 'foo'
  }
};

So here we specified that our User is a generic interface and we can pass data type inside. Of course Typescript can guess it on it's own or we can call an interface with type if we want.

const user: User<{meta: string}> = {
  name: "Jack",
  data: {
    meta: 'foo'
  }
};

So here we specified that data inside this specific user should be an object with property meta.

Now we can create other user and pass inside completely different data.

const user2: User<string[]> = {
  name: "John",
  data: ['foo', 'bar', 'baz']
};

One more thing is that as with parameters in the function you can pass more then one type in the generic and it's completely fine. For example let's pass more types inside UserInterface.

interface User<T, V> {
  name: string;
  data: T;
  meta: V;
}

So here we used 2 of them. You can use how much you need but it's the same like with parameters. More than 2 or 3 arguments is too much.


Now let's look on the real example and check if we can read them now.

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ramda/index.d.ts#L166

export function append<T>(el: T, list: readonly T[]): T[];

Actually you might ask why we have here type definitions of the same function several types. Actually it's an overloading and it's a topic for other video. For now let's focus just on understanding Typescript types.

In the first example we see that append is a generic function. This means that it works with different types. As a first argument we have el which is exactly this different type and a second argument is a list of this type. And additionaly we can say that it's readonly. This is really nice to avoid mutations. Back from the function we are getting array of the same type. So usage will look like this.

const updatedArr = append('foo', ['bar', 'baz'])

Here we append string to the array of string and we are getting a string array back.

Now let's look on the second example

export function append<T>(el: T): <T>(list: readonly T[]) => T[];

Here we have generic function append and it gets only single argument of some type. Back we are getting a generic function which accepts single argument a readonly array of same type and this function returns an array of the same type.

Here is how we can use it.

const updatedArr = append('foo')(['bar', 'baz'])

Now let's look one one more example

export function any<T>(fn: (a: T) => boolean, list: readonly T[]): boolean;

So here is a generic function which gets a function as a first argument withe element of this type. And this function must return boolean. The second arguemtn is a list and this function returns boolean.

Now let's look on the crazy function that I showed at the beginning.

export function compose<T1>(fn0: () => T1): () => T1;

Here is a compose function which has a function as an argument. The only important thing that it should return the data of the same or defined type. Back it gives a function which returns the same time.

So

const newFn = compose<string>(() => 'foo')
console.log(newFn())

Now we have exactly the same function but for different amount of arguments or inner functions.

javascript
export function compose<T1, T2, T3, T4>(fn3: (x: T3) => T4, fn2: (x: T2) => T3, fn1: (x: T1) => T2, fn0: () => T1): () => T4;
`

So here we have 4 Types. So the first argument is a function and it gets 1 argument of type T3 and returns something of T4 type. The second argument is also a function which gets T2 and returns T3. And so on. At the end we are getting a function which returns T4.

const newFn = compose(
  (x: T3) => T4,
  (x: T2) => T3,
  (x: T1) => T2,
  () => T1
)
console.log(newFn())

So actually the idea is that we transform data from one type to another one by one in sequence.

const getSlug = R.compose(
  (str) => encodeURIComponent(str),
  (lowercasedStrArr) => lowercasedStrArr.join('-'),
  (strArr) => strArr.map(el => el.toLowerCase),
  (str) => data.split(' ')
)
console.log(getSlug('this is some title'))

So this was quite complex type definition. As you can some you genetic knowledge now you can read it.

So generics are super important part of Typescript which you must understand and use correctly.

Also if you want to improve your programming skill I have lots of full courses regarding different web technologies.

📚 Source code of what we've done