Search code examples
typescriptclassimplements

Why the need to redefine properties when implementing an interface in TypeScript?


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;
   }
}

Solution

  • 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