DEV Community

Krzysztof Żuraw
Krzysztof Żuraw

Posted on

TypeScript classes, interfaces and all between

If you want to get those posts 1 week before those on dev.to you can join my newsletter on https://krzysztofzuraw.com/newsletter.

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));
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

which translates roughly to JSON:

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

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;
}>;
Enter fullscreen mode Exit fullscreen mode

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' };
Enter fullscreen mode Exit fullscreen mode

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

type Props = ReturnType<typeof mapStateToProps> & typeof dispatchProps & OwnProps;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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.

If you want to get those posts 1 week before those on dev.to you can join my newsletter on https://krzysztofzuraw.com/newsletter.

Top comments (0)