0

I was to create a TypeScript interface that not only requires that particular attributes are present, but also prohibits attributes that are not part of the definition. Here is an example:

  export interface IComponentDirective {
    scope : any;
    templateUrl : string;
  };

  var ddo : IComponentDirective = {
    scope: {
      dt: '='
    },
    templateUrl: 'directives.datepicker',
    controller: function() {
      console.log('hello world');
    }
  };

Even though controller is not defined in the interface, the ddo assignment doesn't throw an error. Doing some research, this looks like it might be as designed:

Notice that our object actually has more properties than this, but the compiler only checks to that at least the ones required are present and match the types required.

http://www.typescriptlang.org/Handbook#interfaces

Notice, however, that after I declare ddo as a IComponentDirective, if I try something like:

ddo.transclude = false;

The transpiler will complain with:

2339 Property 'transclude' does not exist on type 'IComponentDirective'.

Is there any way to enforce a tighter contract?

Dan Caddigan
  • 1,578
  • 14
  • 25

1 Answers1

1

In short (but depending on your definition of "tighter") you can't restrict things beyond this.

The interface is a contract that says "if these members are not present, you aren't one of these". If your object happens to have additional members, that's fine; when you are using the interface, they are invisible to you.

For example (based on your code), when you type ddo. it will only suggest the members on the interface, because you have told the compiler to use the interface type.

You can't use an interface to prevent a member being defined. I can't think of any language that does this off the top of my head. For example, in C# you can implement more than the interface requires you to implement, but when you are using the interface type, those other members are not available.

Adding Properties Dynamically

The question as to why ddo.tranclude = false; generates a warning is a bit different. This isn't related to interfaces - it will do it when there is no interface:

  var ddo = {
    scope: {
      dt: '='
    },
    templateUrl: 'directives.datepicker',
    controller: function() {
      console.log('hello world');
    }
  };

  ddo.transclude = false; // Nope

The reason for this is... this is the point of TypeScript! It is warning you that you might have typed transclude incorrectly. Perhaps you meant templateUrl, which does exist. If TypeScript didn't warn you about this kind of problem, it would allow you to introduce typographical bugs into your code.

So TypeScript will generate a type for any object you create and then enforce that structure, unless you tell it otherwise.

If you wanted there to "sometimes be a transclude member" you can make that possible:

interface SometimesTransclude {
    scope: { dt: string};
    templateUrl: string;
    controller: () => void;
    transclude?: boolean;
}

  var ddo: SometimesTransclude = {
    scope: {
      dt: '='
    },
    templateUrl: 'directives.datepicker',
    controller: function() {
      console.log('hello world');
    }
  };

  ddo.transclude = false;

Or you can dance straight past the compiler (at your own risk) using:

ddo['transclude'] = false;
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Thanks for the speedy reply! This makes sense in the classical sense of an Interface, but why then would it throw when you do `ddo.transclude = false;`? – Dan Caddigan Apr 28 '15 at 18:57
  • I have added a section on this - as it isn't really to do with interfaces specifically. – Fenton Apr 28 '15 at 19:04
  • Hmmm, I understand the option param, but if it's going to warn me about `transclude`, shouldn't it have warned me about controller? I feel like either it should act like a classical interface or it should warn about unknown attributes, but it seems like it's being inconsistent. Also, I didn't know about the `dancing past the compiler` - this is a little scary. – Dan Caddigan Apr 28 '15 at 20:15
  • Here is the interesting distinction. TypeScript is structurally typed and will generate a type for you when you create a variable - and enforce that generated type. That means you can put anything you like on `ddo` when you first create it - but after that, the type is checked. (I go into a lot of detail about this in my book... hint hint ;) ) – Fenton Apr 28 '15 at 20:18
  • Thanks Steve. I'm not a huge fan of how this works, but I think your description deserves the check mark. :) – Dan Caddigan Apr 30 '15 at 03:18