Interfaces with a twist

Topics: Language Specification
Apr 15, 2014 at 2:18 PM
I'm trying to find a typesafe way to declare mongoose models in node.js / typescript. We have the type definitions setup something like this:
interface IEntity {
  _id?: string;
  name?: string;
  age?: number;
}
Then, when constructing a model...
var x  = new Entity({ name: 'name', age: 90 });
However, this also works:
var x = new Entity({ nme: 'typo' });
I know there are good reasons that interfaces behave like they do now, but in this case I want them to behave a little differently. I want the interface definition to mean "These fields are optional, but only these fields are allowed". This would catch the typo above where the name field wasn't getting set.

The only type safe way I can see to do this now is like so:
var x = new Entity();
x.name = 'name';
x.age = 90;
This will catch typos, but is also really verbose once you start creating sub entities and the like.

Any ideas on how to improve this pattern? Any chance that TS might support interfaces in the way I described some time in the future?
Apr 15, 2014 at 3:31 PM
Edited Apr 15, 2014 at 6:06 PM
AFAIK, interfaces are not much different than any other interface from other languages (such as C#), in which a custom type can contain anything it wants (even typos), as long as the required properties are specified. This also promotes injection of types that have the minimum supported properties.

Anyhow, you could try constructor overloading: http://goo.gl/syiGwO (edit: ignore this, doesn't work actually, except to require at least something is given)
Apr 15, 2014 at 4:44 PM
@jamesnw,

I'm not sure that constructor overloading solves the problem. One could still add something like the following and not have the compiler complain about it:
var x = new Entity({ name: 'name', age: 90, _iid:"typo" });
It's just not feasible to have overloads for all the possible permutations.

I agree with @scriby on this point. In fact this was one of the first problems I had with TypeScript, but so far there has been no actual solution for it.

In practice, this has been a significant source for bugs in the code-base.
Apr 15, 2014 at 5:22 PM
Edited Apr 15, 2014 at 6:04 PM
Ah yes, true. Should have tested it further.

Given that this is JS, it makes sense it works as it does. I know there is a request for "sealed"/"frozen" modifiers because of this as well.
Apr 15, 2014 at 6:01 PM
These requests may help add the neseccary constraints required if ever implemented:
sealed: https://typescript.codeplex.com/workitem/1275
freeze: https://typescript.codeplex.com/workitem/332
Apr 15, 2014 at 6:40 PM
"Freezing" seems like the closest concept, if we take "freezing" an interface to mean that implementors of the interface may not have any features not in the interface. But I would guess "freezing an interface" means "don't allow the interface itself to be extended" later, not necessarily that implementing classes can't have additional features.

I think we're charting new territory here in a sense. For instance, I've never seen interfaces in another language that allow optional parameters. Additionally, I haven't worked in another language where you can insert object literals with "structure" (meaning stuff like { a: { b: [ 2 } }.

I think the combination of both of those things give rise to my current problem, and I think it's pretty unique to TypeScript. Meaning that we may need to invent a new term or concept to meet the need. Maybe "closed" interfaces or something like that, which have the property that implementors may implement optional properties, but may not implement anything outside of the interface.

Thanks,

Chris
Coordinator
Apr 15, 2014 at 7:37 PM
It's fairly straightforward to write a linting tool that can catch the new Entity({ nme: 'typo' }); error. As long as the object literal exists in a place with a contextual type, any extraneous properties of the literal can be flagged as a warning.

As for the idea of a 'closed' interface, it's hard to reason about in a structural type system. Example:
/* closed */ interface Entity {
    id?: string;
}

/* closed */ interface NamedEntity {
    id?: string;
    name?: string;
}

// Error: 'name' is not on closed interface 'Entity'
var x: Entity = { name: 'foo' };

function printEntity(x: Entity) {
    console.log('id is ' + x.id);
}

var y: NamedEntity = { id: 'bar' };
printEntity(y); // OK? Or error?
Should the call to printEntity be an error? Why/how?
Apr 15, 2014 at 8:13 PM
Edited Apr 15, 2014 at 8:17 PM
I think you raise a good point that this feature is more along the lines of a linting tool than a language construct. It should only check object literals, and nothing beyond that.

I do wonder what percent of programs have unexpected typing "gaps" due to this (just because it's different than what someone coming from another strongly typed language might expect). If it's a high enough amount, it might make sense to try to address this in some way in TS core (whether it's just education, compiler flag, whatever). But I'm sure it wouldn't be a priority for a while.

It probably would be easy to write a linting tool... if you have intimate knowledge of the typescript compiler :) It would be nice to integrate into tslint, so I can start on that front.

P.S:

I think structural typing takes some getting used to. Here's another instance that surprised me initially:
class Enum1 {
  constructor(private val: string) {}

  static val1 = new Enum1('val1');
  static val2 = new Enum1('val2');
}

class Enum2 {
  constructor(private val: string) {}

  static val3 = new Enum2('val3');
  static val4 = new Enum2('val4');
}

var x = Enum1.val1;
var y = Enum2.val3;
x = y; //No compile error, because Enum1 and Enum2 are structurally equivalent
//Similar issue where Enum2 can be passed to a method accepting an Enum1
//We worked around this by adding a private member of a different name to each enum so they wouldn't be interchangeable. Not sure if that's the best approach...
Apr 15, 2014 at 9:03 PM
Edited Apr 15, 2014 at 9:17 PM
@Scriby
I haven't worked in another language where you can insert object literals with "structure" (meaning stuff like { a: { b: [ 2 } }.
Well, I'd say this is pretty close (C#):
var obj = new { name = "value",  obj = new {  name = "value" } };
(not to mention dynamic class objects also)

Also, "closing" objects works against any concepts of injection support (what if I want to inject my own custom object with the specified properties? [unless the idea is to force people to be confined to the API restrictions]). The only place I see this being needed is when being used to supply function arguments in object form (usually as options to initialize something). I'm not even sure using interfaces would even be related in this case. Seems like this would/should be a function parameter modifier.
class Entity {
   constructor(sealed e: IEntity) { /*...*/ }
}
Apr 15, 2014 at 9:46 PM
Ah yeah, I forgot C# had anonymous types that you could construct like that. That is fairly similar.

Overall, I'm just looking for a solution to catch typos in object arguments for optional properties. I don't really care about actually restricting the data that can be accepted by the function (who cares if it has an extra property or two?).

In a sense it's kind of funny that this compiles:
function x(args: { b?: number }) {

}

x({ c: 1 });
You accept an args.b? Have an args.c instead. :0

So sealing doesn't quite seem like the right solution to the problem (and would probably make it a pain to work with. Oh, you want to pass an object that happens to have a few extra properties, but has what this method needs? Sorry.)

Perhaps the rule looks more like this:

"When a field is specified in an object literal function argument, it should exist in the interface of the parameter type".

That would catch the "typo" case, and not interfere with anything else.
Apr 15, 2014 at 10:23 PM
Well, "sealed" (in JS terms) just means "this object is fixed to given properties, but properties can be modified". As well, it does not required that "Object .seal()" be called/used at all. It is what is expected (in contract terms only). Changing the rules as mentioned would be a huge breaking change for hundreds of people, and possibly introduce it's own set of bugs (and introduce a double standard). Having people add a modifier means "if you want it, now you can do it", rather that forcing a new change.
Apr 15, 2014 at 11:57 PM
Could you give some examples of things you think my suggestion would break? I was thinking it would be pretty safe, but there might be some cases I'm not thinking about.

Just to be clear, I'm only talking about validation on object literals directly passed to functions or assigned.

For example, x({ c: 1 }) would be subject to validation, but x(y) would not. At the point you're directly specifying an arg that the callee doesn't expect, something seems off.

It could be hidden behind a compiler flag if it's not generally applicable.

Adding a sealed keyword to function definitions is in the same ball park, but not quite what I want. First, it doesn't help with any code I don't control. Second, I really see this as a concern of the caller and not the callee. It's the caller's responsibility to make sure they don't have typos - it seems odd to require a keyword on the callee to prevent typos when calling it.

Thanks,

Chris

Apr 16, 2014 at 3:14 AM
Edited Apr 16, 2014 at 3:15 AM
Could you give some examples of things you think my suggestion would break?
I'm pretty sure someone is passing in object literals with other properties for their own special purposes (somewhere, in one form or another). ;)

I don't see the hang up on the function parameter modifier. Perhaps you misunderstand. It would be no different than specifying an expected type for the parameter. The compiler would see the modifier and enforce the object literals being passed in to conform to the expected object exactly (ignoring optional properties of course - just not allowing new ones). This does make it the concern of the caller, but it's the API developers job to make the rules.
Apr 16, 2014 at 5:29 PM
I'd say that in your scenario, they would augment the function signature with the param being passed (or use "any"). Looks like a good trade-off for catching mistakes at least.

I'd guess we have a difference in perspective, which leads us to wanting to solve the problem differently. I'll try to share a little about where I'm coming from.

I've been using TypeScript for about a year at work in a group that's pioneering the new language. Recently, I made a change to a model and expected it to cause compilation errors. When it didn't, I ran the tests which indeed reported errors. As you can guess, the cause was that I had changed property names that are specified as optional parameters within an object.

Now I wonder how many bugs are in the code due to the same cause. And if I've encountered this, won't others? Or are they like I used to be - blissfully unaware?

Currently we have about 10 people writing typescript at my company. By next year it may be 20. Within 5 years it may be 100. We will produce hundreds of thousand or millions of lines of code. Having to get everyone to sprinkle a keyword on every object parameter with optional params on every function to avoid mistakes when calling it is not a palatable solution. And that's only on the code we control.

I understand TS is at 1.0 and rightfully doesn't want to break existing code. This is a sticky problem, and perhaps there are even better solutions than those we've explored. It may be something that gets tackled in the future based on similar feedback from others.

Thanks,

Chris


Apr 16, 2014 at 6:50 PM
BTW: I don't disagree with you (I do see your point). In fact, I'm more inclined to agree with you. :) If one was to consider the benefits of adding errors/warnings to object literals passed in as function arguments, it may do more to help prevent bugs than cause them. In fact, anyone whose code broke would simply have to force it using "<any>" (to allow special cases). I guess it comes down to the flexibility of the compiler and type system to handle the special case of literals as arguments. The modifier idea was to limit possibilities of breaking code, and hopefully provide an easier "flag" for the compiler to enforce a type; but, implicit direct literal enforcement may be the best route for common JavaScript scenarios (if bug catching is a priority over simplicity).

Personally, I would never be caught dead using object literals in place of function arguments because I'm a stickler for performance. :) (and not taxing the GC in special cases [such as game development]) In all my years of JS development, I've never had need of it myself. I do see the appeal of named arguments (typically why object literals are used), as I use named arguments in C# as well. Perhaps TypeScript can support this sometime also, and fill in the other arguments with void(0). ;) (useful only in TS I know)
Apr 24, 2014 at 5:22 AM
Maybe I've been doing line-of-business apps for too long where the performance/memory overhead of the named arguments pattern is "nothing" :)

It really has grown on me over the years. The readability / "structurability" is nice, and of course the fact that it can be updated without breaking all the call sites or requiring funky argument detection code is nice (or really nice in pure javascript).

And then add a document oriented database and mapper, and you've got a lot of functions that are going to take complex structures as arguments.

Thanks for the discussion. I'm happy if I've effectively communicated the need and the team can consider it going forward as it gathers more community feedback.