Search code examples
typescript

Typescript Index Signature Confusion


interface Units {
  [key: number]: object;
}

class Unit implements Units {
  [key: number]: object;
  constructor() {
    for(let x=1; x<=9; x++) {
      this[x] = {};
  }
  }
}

Initially I tried creating the interface 'Units' to implement the class Unit. However, when I do this the 'Unit' and 'this[x]' are underlined in red. When I mouse over 'Unit' VS Code says:

Class 'Unit' incorrectly implements interface 'Units'. Index signature for type 'number' is missing in type 'Unit'.ts(2420)

...and when I mouse-over this[x] I get this message:

Element implicitly has an 'any' type because expression of type 'number' can't be used to index type 'Unit'. No index signature with a parameter of type 'number' was found on type 'Unit'.ts(7053)

I searched around for a solution and that's how I stumbled upon putting the index signature directly into the class. This removes both errors!

Why? Don't these both do the same thing? Why can't I specify the index signature with an interface on this class? I suppose I found a solution but I'd really like to understand the whole situation better.


Solution

  • See microsoft/TypeScript#7633 for an official answer to this question.


    An implements clause on a class declaration does only one thing: it checks whether the class instance conforms to the interface, and if it doesn't, causes a compiler error. It has no effect whatsoever on the class instance type:

    interface Foo {
        a: string;
    }
    
    class Bar implements Foo { // error!
        //~~~ <-- Class 'Bar' incorrectly implements interface 'Foo'.
        //   Property 'a' is missing in type 'Bar' but required in type 'Foo'.
    
        method() {
            this.a; // error!
            //   ~ <-- Property 'a' does not exist on type 'Bar'.
        }
    }
    

    Writing class Bar implements Foo {} asks TypeScript to check Bar to see if it implements Foo. It doesn't cause Bar to implement Foo. Since Foo requires an a property that Bar has not declared, there's an error. And since there's no a property, you can't write this.a either. If you want that to work, you need to explicitly declare a, so that the check passes:

    class Bar implements Foo { // okay
        a = "xyz"; // declare a to be a string
        method() {
            this.a; // okay           
        }
    }
    

    Since implements is only a check and doesn't affect the type of the class, and since types are structural and don't need to be explicitly declared, that means you can simply remove the implements clause without affecting the behavior of the program (the only possible change is that you won't immediately be alerted if the class implementation fails to match the interface):

    class Bar {
        a = "xyz";
        method() {
            this.a;
        }
    }
    const foo: Foo = new Bar(); // still okay
    // Bar implements Foo whether it's declared or not
    

    So really, the only possible use for an implements clause is to get a warning right on your class declaration if you fail to implement the interface. It's actually not terribly useful. If you never try to assign a Bar to a Foo then it's sort of academic whether or not Bar implements Foo. On the other hand if you do assign a Bar to a Foo anywhere in your program, then the success or failure of that assignment gives you the same information as the success or failure of the implements clause. You might prefer to see an error close to your class declaration rather than at some distant part of your code that simply uses the class. So an implements clause has its uses. But it doesn't do very much.


    The same holds for index signature:

    class Unit implements Units { // error!
        //~~~~ <-- Class 'Unit' incorrectly implements interface 'Units'
    
        constructor() {
            for (let x = 1; x <= 9; x++) {
                this[x] = {}; // error!
                //~~~~~ <-- No index signature 
            }
        }
    }
    

    That fails the implements check because Unit has no index signature and Units requires it. And since there's no index signature, this[x] is an error also. You fix it by adding the index signature:

    class Unit implements Units { // okay
        [key: number]: object;
        constructor() {
            for (let x = 1; x <= 9; x++) {
                this[x] = {}; // okay
            }
        }
    }
    

    And again, there's no need for implements Units:

    class Unit {
        [key: number]: object;
        constructor() {
            for (let x = 1; x <= 9; x++) {
                this[x] = {};
            }
        }
    }
    const u: Units = new Unit(); // still okay
    // Unit implements Units whether it's declared or not
    

    That's exactly analogous. So your problem isn't so much an "index signature confusion", but an "implements clause confusion".

    Playground link to code