DEV Community

React GraphQL Academy
React GraphQL Academy

Posted on • Originally published at reactgraphql.academy on

A TypeScript tale  - Interfaces, Classes & Generics

Table of contents:

So far, in the previous episodes, we have covered the various basic types but will come to a point we’ll need to handle them in a more efficient and less verbose way.

Interfaces

Many times we found ourselves repeating code and not being enough explicit to help other developers to read our work. Let’s look at an example:

const introduction = (firstName: string, age: number) =>
    console.log(`My name is ${firstName} and I'm ${age} old`)

introduction(“Francisco”, 36)

We must describe the shape of our object and sometimes this can be quite tedious to look at from our colleagues perspective. In TypeScript, interfaces are a powerful way of defining contracts within your code and turn it more readable. And, for me, the best use for interfaces is when we want to reuse the same object/shape. Let’s have a look at how would it be if we were using an interface for the function introduction:

interface Me {
    firstName: string
    age: number,
    isHuman?: boolean
}
const me: Me = { firstName: "Francisco", age: 36 }
const introduction = ({ firstName, age }: Me) =>
    console.log(`My name is ${firstName} and I'm ${age} old`)

introduction(me)

The same interface can be exported and used it in other functions/objects with the same shape within our project.

Now, if we think about it, this is not the correct way to determine my age. Afterall, the age is not static and changes over time. Let’s create a dynamic algorithm that can be used in the future to establish our age:

interface Me {
  firstName: string
  yearOfBirth: number,
  isHuman?: boolean
  age(yearOfBirth: number): number 
}

const age = (yearOfBirth: number) => {
  return (new Date()).getFullYear() - yearOfBirth
}

const me: Me = { firstName: "Francisco", yearOfBirth: 1983, age}

const introduction = ({ firstName, age }: Me) =>
  console.log(`My name is ${firstName} and I'm ${age(me.yearOfBirth)} old`)

Have you noticed that now we are passing a function in our interface? Again, we are defining a contract to determine a shape, but this time, for the function also.

I usually use interfaces for functions whenever I’ve got more than a single argument or I suspect I’ll be reusing them in the future. At the end, many of our choices are based on scalability. I enjoy keeping my code organised and easy to read but it might be contra-productive to write an interface when all we have is a single argument for a function that we are sure is going to be used only once. I also follow the ‘Colocation’ principle (read more about this paradigm in the React official documentation) where we keep files that often change together close to each other. In the end, this is always a preference and you should write what it feels right for you and your project/peers.

Optional properties

We just created a new type called Me and this will define the shape of our values. If you noticed, our isHuman argument has a ? such that:

isHuman?: boolean

This handy feature means this argument is optional. We don’t need to represent it but, in case we do, it’d be a boolean value.

Classes

Classes were introduced in JavaScript in ES2015/ES6 as a form of syntactic sugar over the prototypal inheritance. Classes should be familiar to any developer that has ever worked with Object-Oriented Programming (OOP) languages. Chances are that, if you follow a more functional programming approach, you won’t find much use for a class .

Let’s look at an example:

class Me {
   name: string
   age: number
   isHuman: boolean
}

Exactly, it looks like an interface, but let’s look at what we can do with our class. First, all fields are public by default, but we can set it as private or protected:

class Me {
   public name: string
   private age: number
   protected isHuman: boolean
}

The same way we use a constructor in JavaScript to initialise fields, we also use it in TypeScript:

class Me{
  name: string
  age: number
  constructor(theName: string, theAge: number) {
    this.name = theName
    this.age = theAge
  }
}

To dive deeper into Classes I’d suggest you have a look at the official documentation. It’s a major topic that follows the OOP paradigm and I won’t be using it in our examples/projects.

Generics

Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.” Wikipedia

One uses Wikipedia for all sorts of searches but this definition of Generics is not the clearest. The idea behind Generics is to provide relevant type constraints between members. This concept is used in many languages such as Java, Python, Rust (and many more), but, since JavaScript is a dynamically typed language, Generics are not available in it.

Some of the benefits of the use of Generics are:

  • Enable types to act as parameters.
  • Defines a relationship between input and output parameters types.
  • Stronger type checks at compile time.
  • Reusability. Enabling the developer to implement generic solutions with reusability in mind.
  • Improves maintainability.

Let’s have a look at some examples where we could take advantage of Generics :

const me = (personality: number): number => {   
   return personality;
}

In the above case, we’ve got a variable me where we pass personality as an argument and return it. We’ve specified our type as a number but what about if we want to change the type in the future? In this approach, we are constraining the function to a single-use type. There’s always the possibility of using the any type but that would come with all the well-known disadvantages of the any type. It would pretty much feel like ‘cheating’ our compiler.

const me = (personality: any): any => {   
   return personality;
}

What we want from this function is to accept an input, and have an output always with the same type. So we do:

function me<T> (personality: T): T {   
   return personality;
}

You might ask two questions:

1 — Why have we turned to pre-ES6 functions? For those who are familiarised with React, we know that when using JSX, a pair of brackets with a capital letter inside might be a Component. Saying this we’ll need a way to tell the compiler that we are defining a generic instead of a JSX element. We can do it in the following way:

const me = <T extends {}> (personality: T): T => {   
   return personality;
}

Generics can extend other Generics or types. In this case, we trick the compiler extending to an empty object. But I’ll go back to the old-style functions for the sake of readability.

2 — Why are we using the Generic type <T>? It happens that we can replace it with any valid name. The letter ‘T’ stands for ‘Type’ and has been used for convention purposes. It’s now used as a placeholder and acts as a type. We can also add more types such as ‘U’. Let’s see:

function me<T, U> (personality: T, mood: U): T {   
   return personality;
}

In our example, we define a set of two parameters, each one with its own type. But we are only returning ‘personality’. To make use of all parameters we can add a tuple defining the types we want returned.

function me <T, U>(personality: T, mood: U): [T, U] {   
   return [personality, mood];
}

Although it looks like a good idea, I honestly don’t think we should be using a tuple in this situation. It wouldn’t hurt as long as we are completely confident we won’t be using this function again but, in a project, how would we know we won’t need to extend or refactor in the future?

Let’s bring forward our previous knowledge about interfaces:

interface Person<T, U> {
   name: T;
   age: U;
   fn(): U;
}
let me: Person<string, number> = {
   name: "Francisco",
   age: 36,
   fn: function() {
       return 3;
   }
};

Here we also define the shape of both the input and output. It’s notorious the true power of combining these two concepts into one and making use of Generic Interfaces to improve, not only, readability, but also reusability.

One great feature of Generics is the ability to have default parameters such as those introduced with ES6. In the function below, in the absence of an explicit type, it will pick the one set as default:

interface A<T=string> {
   name: T
}
const a:A = { name: "Francisco" }
// or...
const a:A<number> = { name: 1 }

On this episode, we’ve seen how powerful our code can become with interfaces, classes and generics. These are major topics and they deserve an episode on their own. But, as we have been doing since episode 1, we’ll keep it simple and will add more detailed specifications along with the story. In the next episode, we’ll dig into advanced types in Typescript. See you there.

Related articles

Previous:

A TypeScript Tale - Episode 1

A TypeScript tale - How to setup and configure

A TypeScript tale - The almighty types

Top comments (0)