DEV Community

Cover image for Typescript Interface vs Class With Practical Examples
Raj Sekhar
Raj Sekhar

Posted on

Typescript Interface vs Class With Practical Examples

Typescript, oh I love it. Take stackoverflow survey, or ask any developer, most of them do. All major UI libraries/frameworks are (following Angular way) adding Typescript support now. Need to write a little extra boilerplate (use json to ts extension), but the benefits of type checking, intellisense and instant visual feedback outweigh the extra work.

I had this confusion where both interface and class gets the work done, but which one to use and when?

TLDR

Use interface, avoid class unless there is any special requirement that cannot be done with interface.

Classes add to js file size, after compiling .ts to .js, while interfaces do not

Classes take extra lines

Lets take scenario, where we want to give structure to a pizza object. I can use interface or an object.

Pizza Interface

pizza-interface.ts

interface Pizza {
    variant: string;
    size: string,
    price: number;
    extraCheese: boolean;
    takeAway: boolean;
}

const myPizza: Pizza = {
    variant: 'Maxican green wave', size: 'medium', price: 550, extraCheese: true, takeAway: false,
}
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode

pizza-interface.js

var myPizza = {
    variant: 'Maxican green wave', size: 'medium', price: 550, extraCheese: true, takeAway: false
};
console.log(myPizza);

Enter fullscreen mode Exit fullscreen mode

Pizza Class

pizza-class.ts

class Pizza {
    variant: string;
    size: string;
    price: number;
    extraCheese: boolean;
    takeAway: boolean;

    constructor(variant: string, size: string, price: number, extraCheese: boolean, takeAway: boolean) {
        this.variant = variant;
        this.size = size;
        this.price = price;
        this.extraCheese = extraCheese;
        this.takeAway = takeAway;
    }
}

const myPizza = new Pizza('Maxican green wave', 'medium', 550, true, false);
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode

pizza-class.js

var Pizza = /** @class */ (function () {
    function Pizza(variant, size, price, extraCheese, takeAway) {
        this.variant = variant;
        this.size = size;
        this.price = price;
        this.extraCheese = extraCheese;
        this.takeAway = takeAway;
    }
    return Pizza;
}());
var myPizza = new Pizza('Maxican green wave', 'medium', 550, true, false);
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode

More the lines in your .js, more is its size

Usecase for class

Lets take a scenario of employee salary, where HRA, PF contribution is dependent on the basic amount. So if I want to provide structure for salary object with least effort, I might go with class instead of interface here.

salary.ts

class SalaryComponents {
    basic: number;
    pf: number;
    hra: number;
    professionalTax: number;

    constructor(basic: number, state: string) {
        this.basic = basic;
        this.hra = basic * 0.5;
        this.pf = basic * 0.12;
        this.professionalTax = this.getProfessionalTax(state);
    }

    getProfessionalTax(stateName: string): number {
        return 2000; // dummy value
    }
}

const emp1 = new SalaryComponents(1000, 'Tamil Nadu');
console.log(emp1); 
/** Output
    {
        basic: 1000,
        hra: 500,
        pf: 120,
        professionalTax: 2000
    }
 */
Enter fullscreen mode Exit fullscreen mode

With just 2 inputs, i could create an object. Pretty neat huh!!

This is the only scenario is could think of, where class is more effective. Hope it was helpful. I am open for any constructive criticism/feedback.

PS: I am looking for new opportunities in Angular. If you have any openings, I am just a message away. (krj2033@gmail.com) (linkedin)

Discussion (16)

Collapse
jfbrennan profile image
Jordan Brennan

I'm confused. Why would your pizza-class.js code not be:

class Pizza {
  constructor(variant = '', size = '', price = 0, extraCheese = false, takeAway = false) {
    this.variant = variant;
    this.size = size;
    this.price = price;
    this.extraCheese = extraCheese;
    this.takeAway = takeAway;
  }
}

const myPizza = new Pizza('Maxican green wave', 'medium', 550, true, false);
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode
Collapse
peerreynders profile image
peerreynders • Edited on

In this context class is a waste i.e. it isn't carrying its weight. On a personal level I'm not fond of classes as they tend to conflate type space and value space (just like TypeScript does with regular function declarations and expressions)

On introduction I'd go with

// Type definition in the type (TypeScript) space
type Pizza = {
    variant: string;
    size: string;
    price: number;
    extraCheese: boolean;
    takeAway: boolean;
};

// Value creation in value (JavaScript) space
const myPizza: Pizza = {
    variant: "Maxican green wave",
    size: "medium",
    price: 550,
    extraCheese: true,
    takeAway: false
};
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode

By going with an object literal TypeScript can verify that myPizza conforms with the Pizza type and as a bonus the object properties act as "named parameters". For example variant and size are both string but there is no confusing them as you have to refer to them by name.

Creating Pizza's all over the place I'd move to:

// Type definition in the type (TypeScript) space
type Pizza = {
  variant: string;
  size: string;
  price: number;
  extraCheese: boolean;
  takeAway: boolean;
};

type MakePizza = (
  variant: string,
  size: string,
  price: number,
  extraCheese: boolean,
  takeAway: boolean
) => Pizza;

// Value creation in value (JavaScript) space
const makePizza: MakePizza = (
  variant = '',
  size = '',
  price = 0,
  extraCheese = false,
  takeAway = false
) => {
  return {
    variant,
    size,
    price,
    extraCheese,
    takeAway,
  };
};

const myPizza = makePizza('Mexican green wave', 'medium', 550, true, false);
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode

Now I can mass produce Pizza objects - yet don't have to bother with either new or this (not that they'll slow me down if it came to it).

However there is a trade off by going with a factory function (or a for that matter a constructor) - TypeScript won't be able to detect whether you swapped variant with size or extraCheese with takeAway.

Bottom line: I don't go "class-oriented" until there is a clear benefit - until then I'll stay function-oriented.


As to the issue of not using parameter properties - one could simply write:

class Pizza {
  constructor(
    public variant: string = '',
    public size: string = '',
    public price: number = 0,
    public extraCheese: boolean = false,
    public takeAway: boolean = false
  ) {}
}

const myPizza = new Pizza('Mexican green wave', 'medium', 550, true, false);
console.log(myPizza);
Enter fullscreen mode Exit fullscreen mode

Is it convenient? Perhaps.
Is it correct? Not in my book.

A type is a specification that a value has to conform to, to be considered a member of that type. It's a "target" that should be spelled out in explicit detail separately and prominently - not be buried inside the code that is responsible for creating the value.

Collapse
jwp profile image
John Peters

For all your examples, does intellisense work on each?

Thread Thread
peerreynders profile image
peerreynders

Are you familiar with Does Visual Studio Rot the Mind?:

I don’t need to remember anything any more. IntelliSense will remember it for me. Besides, I justify to myself, I may not want those 60,000 methods and properties cluttering up my mind. My overall mental health will undoubtedly be better without them, but at the same time I’m prevented from ever achieving a fluid coding style because the coding is not coming entirely from my head. My coding has become a constant dialog with IntelliSense.

So I don’t think IntelliSense is helping us become better programmers. The real objective is for us to become faster programmers, which also means that it’s cheapening our labor.

So if IntelliSense can't keep up - that's its problem . And no, I don't use Visual Code (The IDE Divide).

Collapse
jwp profile image
John Peters

Good point, and my thoughts too.

Collapse
raj_sekhar profile image
Raj Sekhar Author

Actually I did not define any target in tsconfig and by default it compiled to ES5 probably.
Let me check this one again. Thanks for pointing out.

Collapse
jfbrennan profile image
Jordan Brennan

Oh you're showing the compiled output. I thought that was an example of js src

Collapse
lukeshiru profile image
LUKESHIRU

Nice! 3 things I would like to add:

  • You can infer the "interface" from the object itself if you need to:
const myPizza = {
    variant: "Maxican green wave",
    size: "medium",
    price: 550,
    extraCheese: true,
    takeAway: false
};

export type Pizza = typeof myPizza;
Enter fullscreen mode Exit fullscreen mode
  • It was teased a little in the previous item, but you can use type instead of interface:
export type Pizza = {
    variant: string;
    size: string;
    price: number;
    extraCheese: boolean;
    takeAway: boolean;
};

const myPizza: Pizza = {
    variant: "Maxican green wave",
    size: "medium",
    price: 550,
    extraCheese: true,
    takeAway: false
};
Enter fullscreen mode Exit fullscreen mode
  • Not only using interfaces is better for the compiled version of your TS, but also using import type. So we can import the Pizza type as follows:
import type { Pizza } from "./Pizza";
Enter fullscreen mode Exit fullscreen mode

And that line will get removed in the compiled version :D

That's it! Cheers!

Collapse
raj_sekhar profile image
Raj Sekhar Author

Type actually looks better than interface.. so for api response and request object structure, I'll be using type. And interface only to implement any class. Thanks for your time and explaining everything.

Collapse
peerreynders profile image
peerreynders

I personally stick to type aliases. interface comes in handy when it is necessary to augment existing classes with an interface merge:

class MyClass {
  readonly #value: number;

  constructor(value: number) {
    this.#value = value;
  }

  get value(): number {
    return this.#value;
  }
}

interface MyClass {
  square(): number;
}

function square(this: MyClass): number {
  const value = this.value;
  return value * value;
}

MyClass.prototype.square = square;

const container = new MyClass(42);
console.assert(container.square() === 1764, "Square isn't 1764");
Enter fullscreen mode Exit fullscreen mode
Collapse
captainyossarian profile image
yossarian

You can use public keyword in your constructor to avoid boilerplate code

Collapse
ssimontis profile image
Scott Simontis

Have you considered dependency injection as a scenario? For example, what if I have a service class I depend on? If I inject that into my class constructor, I can easily test it.

Collapse
lalves91 profile image
LAlves91

Hi! I believe this scenario doesn't work anymore. If I'm not mistaken, since Angular 9 or 10, you can't use Angular features (like DI, Inputs/Outputs and Lifecycle hooks) in non-annotated classes.

So, for models, I believe interfaces are the best way to go, unless you have custom behaviour/properties (kinda like transient properties in Java entities, which are calculated from base properties).

Collapse
ssimontis profile image
Scott Simontis

I apologize, I am not familiar with Angular and meant my comment as a general note for ES6/TypeScript. I appreciate your insight on Angular mechanics!

Thread Thread
lalves91 profile image
LAlves91 • Edited on

Heey, no need to apologize! It's all about sharing knowledge! Cheers!

Collapse
bentaly profile image
Ben Taliadoros

There is no need to add the properties on a ts class if they match the constructor. Simply add an access modifier (public for example):
"constructor(public xyz:string) {}"
And it will be automatically added as a property of the class and assigned the value.