TypeScript Design Meeting Notes: 5/23/2014

Topics: Language Specification
Coordinator
May 23, 2014 at 10:08 PM
Edited May 23, 2014 at 10:11 PM
Agenda
  • Overload ordering of globally-scoped functions
  • Type argument inference candidate collection algorithm
  • Overload resolution algorithm tweaks
  • Legal alias merging
  • Infinitely expanding types
  • Losing accuracy during type inference
  • Protected
Overload ordering of globally-scoped functions

The compiler seems to have a bug with how overloads of ambient functions are merged across functions. In the example below:
//A.ts

declare function f(x: number);

// B.ts

declare function f(x: string);
If A.ts is sent to the compiler before B.ts, then the f(x: number) overload gets precedence, even though overloads in all other contexts are merged in reverse order to allow for plugin-style typing.

Recommendation: fix the bug and merge in the correct order. Would need to message that this is a minor design change to resolve the issue.

Type argument inference candidate collection algorithm

Current behavior: During inferential typing of something with signatures for the purpose of generic type inference, we relate the N signatures of the target to the M signatures of the source in an NxM manner. However, only signatures with at least as many parameters as their targets are considered possible sources during this operation.

Proposal: When making inferences between a two types, correlate the signatures in a 1:1 parallel fashion, starting with the last signatures first and proceeding backward until we run out of signatures. We suspect no arity checking is needed at this point. This seems to produce better results, and is faster.

Concern: Disjoint overload sets lead to poor inference. Unclear how to solve that.
Concern: Doesn’t this possibly lead to inference of types that won’t be assignable to the actual calls?

Recommendation: Need to experiment with an implementation of the proposal, and throw enough code at it we can understand any potential shortcomings.

Overload resolution algorithm tweaks

Current behavior: “Provisional binding” occurs during overload resolution. Example: when a function expression with a contextually-typable signature is passed to an overloaded function, the compiler needs to “try” to apply each contextual type and see if it causes a failure, which is potentially very large and quadratic.

Proposal: During overload resolution, first, look at all arguments that aren’t subject to contextual typing of function expressions. Apply overload resolution to these ‘primary’ arguments first, treating ‘secondary’ arguments as being universally compatible. At that point, apply the matching overload and push the contextual type into the secondary arguments and proceed.

This is based on the observation that functions cannot reasonably change their behavior based on the expectations of the body of a function expression. If we do a first pass of the non-lambda components and then do lambda components, we can avoid having to rebind lambda types.

Question: What is a contextually-typable argument?
Answer: It’s based on the lexical shape of the argument

Question: What about non-function contextually-typable arguments?
Answer: Those are ‘primary’ because they are re-typable

This could potentially fix the typing of Array#reduce.

Recommendation: Definitely sounds like potentially a positive change. Will need to investigate impact.

Legal alias merging

Current behavior: It was legal to ‘import’ two times as long as one was value-only and one was type-only. This was not intended and could lead to confusing code.

Proposal: Only one import into a lexical name is allowed.

Applies to both internal and external module imports.

Infinitely expanding types

Current behavior: Illegal and/or confusing. When users get this error message, they may be getting it for perfectly valid types.

Proposal: Detect structural comparison at depths > 10 that are identical in the ‘stack’ of how we got there on both sides. When that stack is sufficiently deep and identical, assume it continues forever and presume that the relationship in question is true. As a thought - at depth > 100, assume the compiler is going to crash and issue an error for diagnostics’ sake.

This may have false positives, but they are likely degenerate and coders would likely have difficulty in reasoning about them.

Losing accuracy during type inference

Given the example:
function forEach<T, U>(array: T[], func: (v: T) => U): U[] {
    var result: U[] = [];
    for (var i = 0; i < array.length; i++) {
        result.push(func(array[i]));
    }
    return result;
}

var strings = ["foo", "bar"];
var addOne = (v: number) => v + 1; 

var result = forEach(strings, addOne);
The problem in the above is that T gets inferred to both string and number, with the best common type being {}. Once it goes to {}, we actually let the more specific 'number' type out of the return because U only has one candidate. This allows a hole in typechecking.

Covariance of function parameters allows { } to be a legitimate inference, so the user never gets the error.

Recommendation: See what happens if we make BCT = {} an error during generic type inference

Protected

What does 'protected' mean in a structural type system? Protected in C# is closely related to the nominal subclassing. Is there a way that this can be encoded into the structural type system we use?

Some motivating examples:
// Calling base class methods from derived when using fat arrow methods
// https://typescript.codeplex.com/workitem/2491
 
class Base {
    // General handler
    // Desired visibility: protected
    public _doHandleEvent(e: MouseEvent) {
        // Do some stuff
    }
 
    // 'this'-safe handler
    public handleMouseEvent = (e: MouseEvent) => this._doHandleEvent(e);
}
 
class Derived extends Base {
    public handleMouseEvent = (e: MouseEvent) => {
        super._doHandleEvent(e);
        // Do some other stuff
    };
}
// Change logic for getters and setters

class Base {
    private _x: string;
 
    // Intended public surface area
    public get x() {
        return this._x;
    }
    public set x(newX) {
        this.changeX(newX);
    }
 
    // Desired visbility: protected
    public _changeX(x: string) {
        // Perform change notification, etc.
        /*...*/
        this._x = x;
    }
}
 
class Derived extends Base {
    public _changeX(x: string) {
        // Perform additional recalculation
        /*...*/
        super._changeX(x);
    }
}
 
Any design would need to allow for overriding of the inherited protected members. We already detect the presence of disallowed names, which we use to prevent shadowing private members.

Recommendation: We need to work a proposal that captures the intent of protected and works for the examples given while retaining the structural flavor of the type system.
May 24, 2014 at 3:39 PM
As ever, thanks for sharing these notes. Really good to see.
May 25, 2014 at 9:36 AM
Edited May 25, 2014 at 9:38 AM
For protected, since anyway private already influences the implementation of subclass and prevents the usage of some names, why not just relaxing the rules on the private namespace to allow subclass to access/override private members ? There is no way at runtime to prevent those members from being accessed and this change would better reflect what really happens and at least it would be more clear that we have to care about private members when subclassing.
May 27, 2014 at 7:23 PM
fdecampredon wrote:
For protected, since anyway private already influences the implementation of subclass and prevents the usage of some names, why not just relaxing the rules on the private namespace to allow subclass to access/override private members ? There is no way at runtime to prevent those members from being accessed and this change would better reflect what really happens and at least it would be more clear that we have to care about private members when subclassing.
Why would you want to turn private into something other than what people expect rather than just adding the protected modifier?

@jonturner Are there any examples of how protected causes problems with the structural type system? The examples provided are essentially "this is why people want protected", which is fine, but doesn't really seem like much of a design challenge as it's essentially the same story for any language that offers something similar (including C# and VB).

Also, Ryan Cavanaugh mentioned in the last language notes thread that you guys had whiteboarded some ideas for union types. I may be wrong, but it sounds like the "Type argument inference candidate collection algorithm" and "Overload resolution algorithm tweaks" are steps towards solving some of the problems that might come up there. Any more color on that topic?
May 27, 2014 at 7:57 PM
Edited May 27, 2014 at 7:58 PM
IMHO, the whole point of public, private, and protected is to help promote intended usage, which I think stands in the spirit of TypeScript. Also, I would take it a step further and say the other modifiers, such as "abstract", "virtual", "override", or "new" for methods, also simply goes toward this same goal.
(why "new"? because I have many times wanted to completely replace a method signature, which is valid JS)
Coordinator
May 27, 2014 at 8:24 PM
Edited May 27, 2014 at 8:25 PM
@MgSam/@fdecampredon

This came towards the end of the meeting, so I wouldn't say the notes reflect the ultimate thoughts here. Instead, they are just the beginning of the thoughts of the topic.

The thought here was that traditional 'protected' infer something about the nominal subclass hierarchy. The classes below you in the hierarchy get a different view of visibility that those that reach you from outside this hierarchy (even if through a handle to an instance of a class that otherwise you have access to):
class Base {
    protected foo(): number;
}

class Derived extends Base {
    constructor() {
        var b = new Base();
        b.foo(); // error
        this.foo(); // no error
    }
}
In a more structural type system, it's not immediately obvious what the type of Base is that would error and not error appropriately (assuming a C#-like version of protected). One possible solution would be to type the 'this' pointer differently than just being that derived class just get a copy of the members they derive from.

We do some of this checking to prevent shadowing privates now, and for the above to work, we'd need to extend that.

The solution may be that we just work very similar to private.

Perhaps a conversation for another thread, but in some sense this opens a bit of Pandora's box. Would we want to support internal, or protected+internal or any lesser used hybrids. Especially considering that in practice these are just design time constraints that evaporate when compiled, moving the developer further away from JavaScript. I'm sure there are both camps of developers who want to be abstracted away from JS and those who don't, and TypeScript has to balance between those worlds.

@MgSam - Good question about the union types and simplifying the type system around candidates and overload resolution. It's possible they might affect each other, but we haven't gone back to talk about union types, yet. Would be interesting if that does make things easier.
May 28, 2014 at 2:03 AM
Interesting, thanks. I'll admit I had to double check what C# does in the scenario you outlined since it does allow you to access private members in that sort of situation. I'd vote for keeping the behavior similar to what C# does, as a big chunk of TypeScript's users come from C# or are at least familiar with it.

I think internal is certainly interesting as well. I personally like internal and/or protected type modifiers, but given the veritable war in the Roslyn forums over "protected and internal", I imagine Anders, et al don't want to think about that again any time soon. If you guys do protected I hope you consider internal at the same time as well as I'd personally use internal more than I do protected.

Thanks for posting these!
May 29, 2014 at 11:22 AM
MgSam wrote:
fdecampredon wrote:
For protected, since anyway private already influences the implementation of subclass and prevents the usage of some names, why not just relaxing the rules on the private namespace to allow subclass to access/override private members ? There is no way at runtime to prevent those members from being accessed and this change would better reflect what really happens and at least it would be more clear that we have to care about private members when subclassing.
Why would you want to turn private into something other than what people expect rather than just adding the protected modifier?
In any other language private members are not a part of the class signatures, they are completely hidden from subclass and external code.
protected members however are always a part of the signature in any language, they are just inaccessible from the outside.

Since with typescript private members are part of the signatures, I already feels like they behave halfway as protected one, so in my opinion, completely transforming them into protected would only be natural.