9

I'm getting into classes and interfaces. And I got one thing about it that annoys me, this is the first time I work with these kind of things so bear with me here..

Let's say I got this interface:

// IFoo.d.ts
export default interface IFoo {
  foo: string;
  bar: number;
}

When I implement it in a class I do the following:

// FooModel.ts
import IFoo from './IFoo';

export default class FooModel implements IFoo {
  foo: string;
  bar: number;

  constructor({ foo, bar }: IFoo = { foo: 'hello', bar: 1 }) {
    this.foo = foo;
  }
}

Why do I have to implement the same properties all over again?

This is basically the same as copy-paste but with a strict convention. Also, I have to type foo and bar a total of 6 times each to get it properly assigned with default optional values, based on an interface.

Is there a more efficient way to do this too?

Edit; I'm trying to achieve the following:

A class with the properties, from which the properties can be used for typescript's checking, like this: interface

export default interface FooDTO {
  foo: string;
  bar: number;
}

model

export interface IFoo {
  foo: string;
  bar: number;
}

export default class FooModel implements IFoo {
  foo: string;
  bar: number;

  constructor({ foo, bar }: IFoo = { foo: 'hello', bar: 1 }) {
    this.foo = foo;
  }
}

controller

export default class FooController {
   public static async readAll(): Array<FooDTO> {
      // some model stuff which maps query to dto
      return Array<FooDTO>result;
   }
}
Thimma
  • 1,343
  • 7
  • 33
  • Why would you define properties on an interface? – Krisztián Balla May 31 '20 at 20:09
  • 2
    @Jenny O'Reilly So I can use them with type assignment, let's say I do `function findById(id: number): Array { ... some fetching here ...; return Arrayresults; }`. it's more for consistency/explicit readability and the IDE infellisense – Thimma May 31 '20 at 20:17
  • Ok. That's fine. But why do you also have a class that implements it? – Krisztián Balla May 31 '20 at 20:48
  • @JennyO'Reilly so I can make a new instance of that for model creation, I want it to have the same properties as the interface and assign it, like: `const foo: FooModel = new FooModel({foo: "hi"})`; this will only manipulate foo and not bar on the new instance, since bar has a default value. – Thimma May 31 '20 at 20:52
  • Why not get rid of `IFoo` including `implements IFoo` and use `FooDTO` as the type in the constructor? – Krisztián Balla May 31 '20 at 21:38
  • @JennyO'Reilly But what if I have different DTO's for post, get and put/patch? – Thimma May 31 '20 at 21:43
  • This is not described in your question. Maybe then you need different model classes too? – Krisztián Balla Jun 01 '20 at 09:08
  • 1
    @JennyO'Reilly I might indeed make a new question which is very clear, I'll probably do it later – Thimma Jun 01 '20 at 09:10

3 Answers3

5

I think the canonical answer to "why do I have to implement the same properties all over again" is in the (increasingly outdated) TypeScript spec:

Note that because TypeScript has a structural type system, a class doesn't need to explicitly state that it implements an interface—it suffices for the class to simply contain the appropriate set of instance members. The implements clause of a class provides a mechanism to assert and validate that the class contains the appropriate sets of instance members, but otherwise it has no effect on the class type.

I added the emphasis above: the implements clause does not affect the class type at all; it doesn't add members to it or change the types of members. All it does is tell the compiler to emit a warning if the class does not conform to the interface.


You may be interested in the GitHub issue microsoft/TypeScript#22815, which suggests that members of implemented interfaces should be copied down into the implementing classes. (The title of the issue is about abstract classes, but the ensuing discussion is not limited to that.) It looks like the sticking point is what to do about optional members (see this comment by the TS team lead). The issue is an older one, but it's still open with a "needs proposal" tag, so if you care a lot about it you might want to go there, give it a , and maybe even give more details on what the behavior should be in edge cases so that it wouldn't be a breaking change.

Inside that issue is a suggested workaround using interface merging to tell the compiler that the class instance interface inherits properties:

interface FooModel extends IFoo { } // merge into FooModel
class FooModel {
    constructor({ foo, bar }: IFoo = { foo: 'hello', bar: 1 }) {
        this.foo = foo;
        this.bar = bar;
    }
}

This is less redundant but might be more confusing than just redeclaring the properties from the interface.


Anyway, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
2

Instead of an interface, you could use an abstract class. Same benefits as an interface, but with the possibility of extending it:

export abstract class IFoo {
  foo: string;
  bar: number;
}

export default class FooModel extends IFoo {
  constructor({ foo, bar }: IFoo = { foo: 'hello', bar: 1 }) {
    this.foo = foo;
  }
}
Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • What are the advantages and disadvantages of this? If looks kinda odd to have a class as an interface – Thimma May 31 '20 at 19:50
  • there are no disadvantages, you can use it as an `interface` as well by using `implements`, but then you have to define the fields as you did before. It's just a nice thingy of typescript – Poul Kruijt May 31 '20 at 19:53
  • 1
    also, when using an abstract class, as long as you don't extend it, it doesn't get included in the emit, so it saves bytes – Poul Kruijt May 31 '20 at 20:20
  • This sounds interesting, does this work with type casting too like an interface? Like: `const NewFoo: IFoo = { foo: "hi", bar: 2 };` – Thimma May 31 '20 at 20:22
  • 1
    @Nurfey definitely, no issues [Playground Link](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAQwEYGcZQQY3pgNglFOASQDEII4BvAKDjgDMKB+ALjjSgEsA7AcwDcdOEgRQ2cHgFcAtkmBQhAXxo1MEHmjgA5YAHdyEdmQpwAvNUYV2AIgAWXGwBoRY9gCY4SgUA) – Poul Kruijt May 31 '20 at 20:54
  • This is not the right way to go about it. In the playground, you have made the properties optional which may not be desired. – jarora Mar 28 '22 at 14:23
0

If you define an interface with only properties you basically defined a data structure type that you can then use to type objects.

For example you could now do this:

const fooObj: IFoo = { foo: "test", bar: 12 };

In other programming languages (eg. Java) it is illegal to define properties on an interface. An interface "normally" defines a way you can interact with an object (meaning methods you can invoke on it). But TypeScript is an exception to this.

Krisztián Balla
  • 19,223
  • 13
  • 68
  • 84
  • I indeed use it for the data structure and DTO's, which is why I need it, but I think I probably shouldn't be using interfaces for DTO's but it works fine and it makes mapping easy – Thimma May 31 '20 at 20:24
  • @Nurfey I understand. But implementing an interface that has only properties make no sense to me. As an interface is like a "contract" the class has to implement. If there are no methods, there is also no contract. – Krisztián Balla May 31 '20 at 20:33
  • Makes sense, is there an alternative for also having type casting with property autocompletion..? – Thimma May 31 '20 at 20:41
  • @Nurfey I am not sure I understand what you are trying to achieve. Maybe you could update your question with some more information on that? Not only the problem you ran into, but what you are trying to achieve. – Krisztián Balla May 31 '20 at 20:44
  • I followed your advice, and I indeed did not need an interface since I can just use the model for the type assignment f.e.: `const foo: FooModel = new FooModel();`, I used to do: `const foo: IFoo = new FooModel();`, the new one just makes much more sense. Thanks! – Thimma Jun 01 '20 at 15:10
  • 1
    @Nurfey thanks for reporting back. In this case one usually doesn't include the type, because it disturbs the readability and is not necessary. So `const foo = new FooModel();` would be enough. Just like `const num = 5;` is enough and you don't need to write `const num: number = 5;` – Krisztián Balla Jun 01 '20 at 15:28