Soundness: some proposals and alternatives

Topics: General, Language Specification
Apr 5, 2014 at 3:17 AM
Edited Apr 5, 2014 at 4:21 AM
There are a few places in the current spec where it looks like the language design had to make a choice between type soundness and ease of using existing javascript libraries. In a couple of instances that I am aware of (function argument bivariance and optional/rest arguments), the choice was made to make it easier to use existing javascript libraries at the expense of soundness.

However, I think a lot of developers would like the option to make the opposite trade-off - preferring soundness over convenience, especially when there is a workaround. I have a few proposals for addressing this (I don't think they are mutually exclusive) while not making it 'all-or-nothing'.

The first is to have a compiler switch that enforces soundness more strictly. In the example of function argument bivariance, enabling the soundness flag would mean that function arguments would succeed only if the source parameter is assignable to the target parameter:
// this is an error with the switch on, but compiles fine with the switch off
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ', ' + e.y));
Now, when you have the soundness switch on and you want to "opt-in" to use the current behavior (e.g. structural bivariance), then I think there could be ways to make this less cumbersome.

First, introduce an 'as' keyword. This keyword would be valid in function arguments only as a modifier to a type:
listenEvent(EventType.Mouse, (e: Event as MouseEvent) => console.log(e.x + ', ' + e.y));
Here, the type of the lambda expression as a whole is (e: Event) => void, but in the body of the lambda, e is considered a MouseEvent. You can use the 'as' keyword for any function argument. For example:
interface IEventListener { listenEvent(eventType: EventType, e: Event): void; }
class MouseEventListener extends IEventListener {  listenEvent(eventType: EventType, e: Event as MouseEvent): void { console.log(e.x + ', ' + e.y); } }
addListener(EventType.Mouse, new MouseEventListener());
if introducing a new keyword is troublesome for whatever reason, maybe something that looks like a cast, but to the left of a type annotation. Slightly uglier, but less likely to conflict?
listenEvent(EventType.Mouse, (e: <Event>MouseEvent) => console.log(e.x + ', ' + e.y));
Another idea would be to introduce the idea of a inferred cast. An inferred cast would work as a regular cast, except that the target type of the cast would be inferred from context. For example:

listenEvent(EventType.Mouse, <>(e: MouseEvent) => console.log(e.x + ', ' + e.y));
would be equivalent to this:
listenEvent(EventType.Mouse, <(e: Event) => void>(e: MouseEvent) => console.log(e.x+ ', ' + e.y));
because the second argument of listenEvent expects an argument of type (e: Event) => void.

Here's another example:
var event: Event = <>getMouseEvent(); // getMouseEvent returns a MouseEvent.
Finally, if you support a "soundness on" switch, there should probably be some scoped way to turn it on or off, similar to the 'use strict' directive. There are probably a few ways to do this, from a keyword, to a 'use strict'; style statement, to an annotation, etc.. Here I'm using a comment:
// file compiled with soundness on
var foo = function() {
  // @Sound(false)
  listenEvent(EventType.Mouse, (e: MouseEvent) => ...etc... );
  if(etc..) {
    // @Sound(true)
    listenEvent(EventType.Mouse, (e: Event as MouseEvent) => etc...);
  // still off here
  listenEvent(EventType.Mouse, (e: MouseEvent) => ...etc...);

// still on here
listenEvent(EventType.Mouse, (e: Event as MouseEvent) => etc....);
[Edit: couple of typos, and I had e: MouseEvent as Event instead of, I think, the more intuitive e: Event as MouseEvent]