Justification for Any being bidirectionally assignment compatible?

Topics: General, Language Specification
Jan 30, 2013 at 2:26 PM
Edited Jan 30, 2013 at 5:54 PM

Hi everyone,

Something that immediately bit me and confused me about TypeScript is that a value with the Any type can be assigned into a variable that has been statically typed more specifically than 'Any'. E.g.

var a : any = {};
var s : string = a; // expected this to be an error

I found this out through a more complicated example where I had fields which were accidentally of the 'any' type (because the compiler couldn't infer anything stronger). The other way makes sense to me, the a variable should be able to hold a string. The language specification does describe this behaviour:

    "A type S is assignable to a type T, and T is assignable from S, if one of the following is true: ... S or T is the Any type."

But doesn't give an explanation as to why this is a desirable feature to have in the language. My gut feeling is that it has something to do with staying a strict superset of JavaScript, but currently I can't see why some sort of explicit cast shouldn't be required in my example above, as is the case when casting from a super type to a base type:

class Animal {
}

class Snake extends Animal {
}

var a : Animal = new Animal();
var s : <Snake> =  a; // type assertion allows this
s = a; // no type assertion, get error that cannot convert Animal to Snake

One thing I've found that at least discusses this behaviour is: http://siek.blogspot.co.uk/2012/10/is-typescript-gradually-typed-part-1.html

Could anyone enlighten me with an example as to why it is desirable for Any to be assignable to a statically typed variable?

Coordinator
Jan 30, 2013 at 4:55 PM

Having Any assignable in both directions comes from the gradual typing world.  Saying 'Any' is like saying "I don't know, this value may come from a place outside of the static type system".  When you don't know, you can't make any assumptions about its contents.  It may be assignment compatible.

To your question, though, why should any be assignable to a statically typed variable?  I think the answer might just be that when you're interfacing with dynamic code, including JavaScript, you'd end up requiring casts often.  This could become a burden on the programmer without giving them much of an improvement in safety (as casts aren't checked at runtime).

The design choices we made for TypeScript tend to favor trying to be useful and not being overly burdensome on the programmer.  This choice seems to have the effect that for most things the language feels pretty light, but there are definitely corner cases where it doesn't quite catch everything. 

 

Feb 1, 2013 at 4:18 PM
In general, this makes sense to me. In a language like C#, 99% of the time your code is dealing with statically typed objects, and if you're doing a lot of casts, you're probably doing something wrong. But in the TS world, you're forced to live in an environment defined by JavaScript's dynamic types. You can often get around this by specifying interface definitions (.d.ts files) and what-not, but not always, and that's not always a reasonable approach. I agree that this is probably the best way to approach the language: what you lose in type safety you more than make up for in convenience. (And in the end, the only measure of the value of any language feature is whether it makes a programmer's life easier.)

That said, I could imagine a "strict" compilation setting (probably as a part of each file) which didn't allow this sort of assignment, or at least, required you to make the cast explicit. Or perhaps a setting that just merely flagged it as a warning. Kind of like VB's "option explicit" or the JS hack "use strict".
Feb 1, 2013 at 6:02 PM
Hi guys, thanks for your thoughtful replies. Couple of thoughts from me, sorry about any non-linearity in them!

I've definitely come at TypeScript with expectations of an experience similar to that in a statically typed language rather than a gradually typed one. I guess there is more justification in the literature about gradually typed languages. :) I admit that I haven't given TypeScript a go for any sizable project yet so I'll definitely try to keep an open mind to this, anything that can help me understand my client side code better seems like a win.
When you don't know [the type of a value at compile time], you can't make any assumptions about it's contents
Jon, that is true, but since an Any is assignable into some value with a static type, does it also follow that you can't make assumptions about the contents of that, too?
I'm trying to shift my mindset from type annotations as providing facts/constraints on runtime values to being more like my expectations (or preconditions?) for values used at runtime, which seems to fit their implications in TypeScript better (though I guess this is also the case in many statically typed languages where type annotations don't put constraints on whether a value is null or not).

I've also been thinking about if a "use strict" mode makes sense in TypeScript. I think maybe I'd be happy with a warning - really I don't want to force myself or others to do unchecked casts, but I probably do want to be kept aware of Any values in my code. This will probably also improve through me gaining more experience about how Any values can make their way into a program.

Overall I feel it's probably right that this isn't an error now, hopefully I'll find some time to better see whether it really is an inconvenience for me or not!
Feb 1, 2013 at 6:05 PM
I had this same thought since Typescript first came out, and I've been curious about that since. This thread offered a bit of an answer, but I still felt there was something more, because I still had a use case where I wasn't a fan of this. Then it hit me. The way it's done now is great if you're writing Typescript from the start. There's no reason to cast something just to get around a purely compile-time limitation. But there's still a use case for it (just look at casting in C). That being that it forces the programmer to actually think through what type the data should be and cast accordingly. And that proves to be very useful when converting code from Javascript to Typescript.

Anyway, that's just my two cents. I would definitely support some type of "strict" compilation option.
Coordinator
Feb 4, 2013 at 4:29 PM
There has been some discussion around something like a "strict" mode. There is experimental support for a some of what the mode might do, which allows you to set the style of the checker on your file to disallow implying 'any', which enforces more type annotations when the types aren't known:
///<style implicitAny="off" />
Again, this is experimental, but may be helpful.
Feb 5, 2013 at 3:34 PM
@itsmonktastic, this is just one more example of TypeScript's lack of type soundness ("well typed programs do not wreck with type errors at runtime"). In you care for my biased opinion, TypeScript designers are making poor choices. There's this talk about gradual typing, but published gradual typing systems, such as this one from Siek & Taha (2006) [1], prove type soundness. In fact, in the PL community soundness seems to be the first and foremost requirement for any type system design.

To me, the obvious choice would be an F# or C#-like type system, which is not sound in presence of downcasts, but is sound if those are avoided. Downcasts allow to explicitly deal with the 1% of places where this is really needed, while preserving guarantees for the 99% of code that does not use them. This is not unlike unsafe* and Dynamic features in Haskell - they are an unsafe hole, but are available for when you need them, and do not render the whole Haskell type system useless.

TypeScript authors are citing convenience as a reason for breaking soundness. But comparing to F# or C#, what it boils down to is allowing the programmer write f(x) instead of f(downcast x). Is THAT the gain bought by making the type system entirely useless to guarantee absence of type errors? If so, it is a rather absurd decision.

[1] http://ecee.colorado.edu/~siek/pubs/pubs/2006/siek06:_gradual.pdf
Feb 7, 2013 at 12:08 PM
I believe the type "any" represents the dynamic type rather than the super type of all types. Hence the code
      var a : any = {};
      var s : string = a; // expected this to be an error
is not really a downcast. It's equivalent to the following C#:
       dynamic foo = new { };
       string bar = foo;
why [is] this is a desirable feature to have in the language
  • Obviously it provides the means to model the true behaviour of JavaScript.
  • It's very useful for testing:
function foo(element:HTMLElement){
    
    element.addEventListener("click", ()=> {});
}

// Test case

// Arrange
var listenerAdded = false;
var mockElement = {
    addEventListener: ()=> listenerAdded = true
};

// Act
foo(mockElement); // Error: Supplied paramters do not match...
foo(<HTMLElement> mockElement); // Error: Cannot convert x to HTMLElement...
foo(<any>mockElement); // Okay

// Assert
assert.isTrue(listenerAdded);
Noel