TypeScript classes, interfaces and all between

March 25, 2019

Hello 👋

What is a better way of explaining classes & interfaces than showing them in action? One of the clever use of classes I saw is this connected to validating response from API.

Most of the current TypeScript or JavaScript application needs to somehow connect to API. How do you parse those JSON and use them in all your application? One of the patterns I use in my project is one using DTOs - it stands for data transfer objects. Why do I need such things as DTOs?

To have your application to operate on the same values and object through all the application, also to validate incoming data to an application.

DTOs in action

If you read 👆paragraph - congratulations 🎉. Let’s jump straight into the code so you don’t get bored:

axios.get<{ user: IUserDTO }>('api/users/1').then(res => UserModel.create(res.data));

I’m using IUserDTO to tell axios what response I may get from API - in my case, it will be:

interface IUserDTO {
  name: string;
  password_hash: string;
  email?: string;
  is_super_user: boolean;
}

which translates roughly to JSON:

{
  "user": {
    "name": "John",
    "password_hash": "42",
    "email": "john@example.com",
    "is_super_user": true
  }
}

In then you may see UserModel.create - this is an entry point to user model to our application.

Models usage

From right on I will use only UserModel in my redux store, selectors or in components. Like here:

// actions
interface getUserSuccess {
  type: 'GET_USER_SUCCESS';
  user: IUserModel;
}

// state
type State = Readonly<{
  user: IUserModel | null;
  isFetching: boolean;
}>;

Types vs interfaces

At the bottom of the snippet is another way of declaring type - type. What is the difference between type or interface? They are used in for the same thing - telling TypeScript what kind of shape and types of values you expect. I personally use interface more often as they allow me to extends itself like:

interface Animal {
  name: string;
}

interface Dog extends Animal {
  isBarking: boolean;
}

const rex: Dog = { isBarking: true, name: 'Rex' };

Yet there are casses where I use type - for example when I have react Props declaration:

type Props = ReturnType<typeof mapStateToProps> & typeof dispatchProps & OwnProps;

Go back to models

I have UserModel class here:

class UserModel implements IUserModel {
  constructor(
    public name: string,
    passwordHash: string,
    public isSuperUser: boolean,
    email?: string
  ) {}

  static create(dto: IUserDTO): UserModel {
    return new UserModel(dto.name, dto.password_hash, dto.is_super_user, dto.email);
  }
}
interface IUserModel {
  name: string;
  passwordHash: string;
  email?: string;
  isSuperUser: boolean;
}

interface IUserDTO {
  name: string;
  password_hash: string;
  email?: string;
  is_super_user: boolean;
}

At the beginning, it may seem that there is a lot going on here but let’s start from the top. First I tell UserModel class to implement IUserModel. IUserModel is the interface. It means that all objects that have the same fields of the same type fulfill interface. In case above when UserModel implements IUserModel is has to have: name, password, isSuperUser & email. I make sure it has in constructor.

If I have an object:

const user: IUserModel = {
  name: 'Evil user',
  password: '',
  isSuperUser: true,
  evil: true,
};

that has additional property (like evil) TypeScript will tell me that:

Type '{ name: string; passwordHash: string; isSuperUser: true; evil: boolean; }' is not assignable to type 'IUserModel'.
  Object literal may only specify known properties, and 'evil' does not exist in type 'IUserModel'.ts

The naming of the interfaces can be the same as the naming of classes that implements those interfaces. Yet I added I as a prefix to denote that I’m using an interface here.

Going back to the constructor. I have there something called public before each one of the arguments to constructor method. This is shorthand for:

class UserModel implements IUserModel {
  constructor(name, passwordHash: string, isSuperUser: boolean, email?: string) {
    this.name = name;
    this.passwordHash = passwordHash;
    this.isSuperUser = isSuperUser;
    this.email = email;
  }
}

Then I can create new class using new keyword: new UserModel('Krzysztof', '134', true, 'kz@example.com').

The last missing piece is static create() method. Keyword static means that method is not bound to instance of the class. So you don’t need to write:

const user = new UserModel('Krzysztof', '134', true, 'kz@example.com');
user.create();

You can use it without instantiating class first - UserModel.create() which I’m using in my API call code. In the body of create I just create a new class to work with.

Summary & TL;DR

I’m using classes with TypeScript interfaces to accomplish: validation of data coming & incoming from the application. Types are also great but for situations where I can glue together different types like interfaces & ReturnTypes.