TypeScript - Class or Interface for Model?

Introduction

Should I use Class or Interface for my models? Hmm.. This used to be one of the questions I had in mind back then when I first started out working with TypeScript. It was 2017 back then when TS was still not that widely adopted yet, while Angular was still somewhat the hottest pancake in town. Haha..

Tons of tutorials and resources out there. However, there are a great mix of choices, opinions and usages for Class vs Interface when it comes to the topic of creating a model.

Motivation

Practically, both Class and Interface can be used interchangeably as "type" in TypeScript. But just because it can be done, does not necessarily mean that you should!

Let's go a level deeper. Question the usages. Uncover the whys! What are the best use cases and practices? Only then, can we grow ourselves into a better developer each day!

In this article, I will try to put in my 2 cents in regards to this topic, with some practical examples drawing from day-to-day experience as a web developer.

But just because it can be done, does not necessarily mean that you should!

Topics Coverage

  1. What is Model?
  2. The Truth of Class
  3. Class vs Interface
  4. Power of Class
  5. The Request-Response Pattern

1. What is Model?

In programming, Model is simply a software representation and encapsulation of a real world subject, or a "thing". It can be used to describe a physical object, an event, or even an internal process, depending on the use case.

In a typical OOP language, a model is often just a class. In the classical term, Java for example, a model is represented by a POJO. In C#, you have your POCO.

Yea, that's it. It is basically just a class, with properties and perhaps some methods that describe the subject itself.

Okay great, what about TypeScript then? Is it also just a class? Or... Hey, wait! Is TypeScript even an OOP? Or should we say, JavaScript instead 😉 Haha..

2. The Truth of Class

Class itself is a syntactic sugar in JS, introduced in ES6. It is actually a Constructor Function in disguise. In the Pre-ES6 era, especially in some legacy JS codes, there is this widely adopted JS creational design pattern called the "Constructor Patterns".

Here is a good writeup about design patterns in JS. Check it out!

The two code snippets below are actually equivalent. The first is implemented with a Class (ES6) and the second, with the Constructor Pattern.

ES6

1class SomeClass {
2    constructor(a, b){
3        this.a = a;
4        this.b = b
5    }
6}
7
8var obj = new SomeClass(1 ,2);
9

Pre-ES6

1var SomeClass = function Test(a, b) {
2    this.a = a;
3    this.b = b;
4}
5
6var obj = new SomeClass(1 ,2);
7

So, to a certain extent, class is actually a function in JS. Though it is a specific one - Constructor Function.

3. Class vs Interface

With TypeScript, there is also Interface! Think of it like a skeleton, or rather a blueprint for an object. It defines the expected structure of an object.

Interface contains no logic. It is just a type definition for an object. You cannot instantiate an instance out of an Interface.

On top of being capable of defining a structure, Class lets us create an object instance of it. With that, we can include additional methods for the object to invoke, as long as it is an object instance created from the Class!

Though, much of it depends on the code implementations. You can still pretty much assign a method to an existing Object's prototype, making it works like an object instantiated from a Class.

4. Power of Class

Enough definitions and talks. Let us write some codes to illustrate the power of Class and the perfect situations for it!

The example will be depicted from a frontend perspective. However, it is applicable for backend too. Just flip it around!

4.1 Case Study

Imagine, you are developing a React application to display the summary of a company's financial information - Revenue, Profit/Loss, Profit Margin, etc.

You have an API that returns you the following data regarding a company's performance:

1{
2    name: 'Codefee Time',
3    performances: [
4        {
5            year: 2019,
6            revenue: 100000,
7            cost: 4000,
8        },
9        {
10            year: 2020,
11            revenue: 80000,
12            cost: 3000,
13        },
14        {
15            year: 2021,
16            revenue: 120000,
17            cost: 5000,
18        }
19    ]
20}
21

This plain JSON object will only get us so much information, regarding the company's revenue and cost in the corresponding years.

What if we need to get the following info:

  • Profit of each year
  • Profit margin of each year
  • Total profit across all years
  • Total profit margin across all years

4.2 Solutions & Thoughts

Alright, how about we write the compute functions each time when we need it? Heck no! It will pollute the code and create maintenance mess. So wetttt.... Not even gonna consider writing it!

4.2.1 Use Helper Class

Hmm.. What about encapsulating the compute logics in a helper class (potentially a custom hook in React)? Hmm.. Maybe or maybe not? Let's see...

First, we have our Interfaces to capture the data structure:

1export interface ICompany {
2  name: string;
3  performances: IPerformance[];
4}
5
6export interface IPerformance {
7  year: number;
8  revenue: number;
9  cost: number;
10}
11

Here, we have a helper class that encapsulates the computation logics:

1import { IPerformance } from "./Interfaces";
2
3export const getProfit = ({ revenue, cost }: IPerformance): number => {
4  return revenue - cost;
5};
6
7export const getProfitMargin = (performance: IPerformance): number => {
8  const { revenue } = performance;
9  return getProfit(performance) / revenue;
10};
11
12export const getTotalProfit = (performances: IPerformance[]): number => {
13  return performances.reduce((a, c) => {
14    return a + getProfit(c);
15  }, 0);
16};
17
18export const getTotalProfitMargin = (performances: IPerformance[]): number => {
19  let totalProfit = 0;
20  let totalRevenue = 0;
21
22  performances.forEach((performance) => {
23    totalProfit += getProfit(performance);
24    totalRevenue += performance.revenue;
25  });
26
27  return totalProfit / totalRevenue;
28};
29

Here is the resulting Component class

1import axios, { AxiosResponse } from 'axios';
2import { ICompany } from './Interfaces';
3import { useEffect, useState } from 'react';
4import {
5  getProfit,
6  getProfitMargin,
7  getTotalProfit,
8  getTotalProfitMargin,
9} from './Helper';
10
11const SomeComponent = () => {
12  const [data, setData] = useState<ICompany>(undefined);
13
14  useEffect(() => {
15    axios
16      .get('some url to api')
17      .then((res: AxiosResponse<ICompany>) => {
18        setData(res.data);
19      })
20  }, []);
21
22  const totalProfit = getTotalProfit(data.performances);
23  const totalProfitMargin = getTotalProfitMargin(data.performances);
24
25  return (
26    <div>
27      <h1>{data.name}</h1>
28      <h2>{totalProfit}</h2>
29      <h2>{totalProfitMargin}</h2>
30
31      {
32        data.performances.map(datum => {
33          return (
34            <>
35              <h3>Year: {datum.year}</h3>
36              <h4>Profit: {getProfit(datum)}</h4>
37              <h4>Profit Margin: {getProfitMargin(datum)}</h4>
38            </>
39          );
40        })
41      }
42    </div>
43  );
44}
45

It is somewhat okay. But then, there is this extra import line(s) that you need to write on all the Components that require the compute functions. On top of that, you have the helper functions all over the place, polluting the Component itself. Hmm.. Somewhat an eyesore to me.

If we think about it for a second... The information that we are trying to compute, are in fact, derivable from the data itself.

In other words, those are actually info that can be, and should be considered as part of the Company and Performance "Models". We can essentially move them into model Classes and encapsulate all the model-related logics inside! Let's take a look at it!

4.2.2 Use Class as Model

Here is our Company and Performance model classes:

1import { ICompany, IPerformance } from "./Interfaces";
2
3export class Company {
4  name: string;
5  performances: Performance[];
6
7  constructor({ name, performances }: ICompany) {
8    this.name = name;
9    this.performances = performances.map((p) => new Performance(p));
10  }
11
12  public get totalProfit(): number {
13    return this.performances.reduce((a, c) => {
14      return a + c.profit;
15    }, 0);
16  }
17
18  public get totalProfitMargin(): number {
19    let totalProfit = 0;
20    let totalRevenue = 0;
21
22    this.performances.forEach((performance) => {
23      totalProfit += performance.profit;
24      totalRevenue += performance.revenue;
25    });
26
27    return totalProfit / totalRevenue;
28  }
29}
30
31export class Performance {
32  year: number;
33  revenue: number;
34  cost: number;
35
36  constructor(data: IPerformance) {
37    Object.assign(this, data);
38  }
39
40  public get profit(): number {
41    return this.revenue - this.cost;
42  }
43
44  public get profitMargin(): number {
45    return this.profit / this.revenue;
46  }
47}
48

Now, we have essentially created Company and Performance models that are capable to provide all necessary info to our Component, be it derived or not.

A few key points to takeaway from these models' implementations:

  1. Performance model - I used Object.assign at the constructor. Though it looks clean, this should be used with care, and only if we are 100% sure that the properties from the Interface should all be reflected in the Class. Otherwise, you might get some runtime null error down the road.
  2. Getter Method - This effectively turned our compute methods into property flavors. It made it as though we are just getting property values from the model itself! Isn't that amazing?!

Here is how our resulting Component will look like:

1import axios, { AxiosResponse } from 'axios';
2import React, { useEffect, useState } from 'react';
3import { Company } from './Company';
4import { ICompany } from './Interfaces';
5
6const SomeComponent = () => {
7  const [data, setData] = useState<Company>(undefined);
8
9  useEffect(() => {
10    // We convert the "Interface data into a Company Model Class"
11    axios
12      .get('some url to api')
13      .then((res: AxiosResponse<ICompany>) => {
14        setData(new Company(res.data));
15      })
16  }, []);
17
18  const {
19    name,
20    totalProfit,
21    totalProfitMargin,
22    performances,
23  } = data;
24
25  return (
26    <div>
27      <h1>{name}</h1>
28      <h2>{totalProfit}</h2>
29      <h2>{totalProfitMargin}</h2>
30
31      {
32        performances.map(({
33          year,
34          profit,
35          profitMargin,
36        }) => {
37          return (
38            <>
39              <h3>Year: {year}</h3>
40              <h4>Profit: {profit}</h4>
41              <h4>Profit Margin: {profitMargin}</h4>
42            </>
43          );
44        })
45      }
46    </div>
47  );
48}
49

To me, this just instantly spins into a poetry in code! It's very declarative, clear and self expressive, with no helper methods being tangled here and there, everywhere in the code (when we don't need them)!

The key is to transform the Interface data (raw JSON from API) into a Model Class.

5. The Request-Response Pattern

Following the examples and lessons from previous section, there is a certain pattern that we can establish here, to help us organize our models better. This pattern has been serving me well so far, in terms of Web Application development, be it Angular or React apps.

Perhaps, there is already a name for this, but well.. We'll just call it the Request-Response Pattern for now.

In a standard Web Application, there are generally 3 types of models that we will usually encounter. We'll name them:

  1. Request model
  2. Response model
  3. App model

Normally, I will group them into their own folders, i.e. Request, Response and App literally for segregation purposes.

By scoping the models into these 3 major groups, we can then establish a clear responsibility for each of them.

This is where the usage choice between Class and Interface becomes apparent as certain model type just fits naturally into one of them.

5.1 Request model

A Request model represents the payload from Client app to Server (API). This refers to the payload usually involved in POST, PUT methods, i.e. the payload to create or update a resource.

This type of model, I generally prefer to use Class over Interface, though it can certainly be done with the latter as well.

Reason being, we might need to derive additional data fields for the payload from user inputs. Depending on implementation, it's possible to do a client-side validation in this model as well before even invoking the API call.

Encapsulating those data deriving and validation logics as part of the request model sounds clean and logical to me. Hence, Class wins for me.

This Request Model corresponds to the "In DTO" for some literatures on backend context.

5.2 Response model

A Response model represents the structure of any data returned from an API, regardless of HTTP methods.

For this type of model, I generally prefer to use only Interface. If we take the use case from section 4, it'll be represented by ICompany and IPerformance interfaces.

Reason being, raw data (JSON data) returned from an API is raw/pure and simply does not have any derived property yet. Therefore, it should be represented as is.

If the needs arise, we can then convert this Response Model into an App Model (as shown in Section 4). I would like to think of that as the transformation of Interface data into a Class model. If that makes sense!

This Response Model corresponds to the "Out DTO" for some literatures on backend context.

5.3 App Model

An App Model represents the model that is being used within an application scope, e.g. State data, etc.

For this type of model, I generally prefer to use only Class.

Most often than not, your application is gonna require some kind of derived data or property from this type of model. Scoping model-related computation within a model Class, keeps your business logics clean and uncluttered.

If we take example from section 4 above, it will be the Company and Performance Classes. It is a data from API (response model) transformed and kept as an App Model, acting as a State within the Component.

Conclusion

Woooooooo~~ didn't expect myself to write that long on this topic! It is certainly a deep one.

So, in my humble opinion, both Class and Interface have their place to really shine in the space of model. To say that it makes no difference to use one over the other, is plain ignorance. They differ wildly in nature, and fits better over the other in certain use cases.

The Request-Response Pattern depicted in this article, is simply a frontend model organization pattern that emerged naturally in my day-to-day job. Similar pattern can be adapted into backend models too if you're working on Node.js. Just flip it around! Anyway, take it or leave it. I would be extremely glad if this enlightens some of you, or even bring that Eureka moment to you! If it happens to be too opinionated, too bad I guess! Haha...

Know your bullet well, and make good use of it. JavaScript/TypeScript itself is an art of a language. Make sure you don't waste it! 😛


Recently, I've been enjoying my Pour-over coffee in this weird way. I keep the coffee in the server, mix them and pour over bit by bit over to my drinking cup 😛

With that, I'm able to sip and appreciate the coffee bit by bit as the temperature falls. No more waiting for it to cool too much! Haha.. To my surprise, the aroma was actually more contained too. Not sure if I'm imagining things though! Haha..

coffee
Coffee Time in the evening!

Resources

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
  2. https://en.wikipedia.org/wiki/Object-oriented_programming
  3. https://www.typescriptlang.org/
  4. https://www.w3schools.com/js/js_object_constructors.asp
  5. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor
  6. https://googlechrome.github.io/samples/classes-es6/
  7. https://www.w3schools.com/js/js_es6.asp
  8. https://www.lambdatest.com/blog/comprehensive-guide-to-javascript-design-patterns/
  9. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get