Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local types #3266

Merged
merged 18 commits into from
May 31, 2015
Merged

Local types #3266

merged 18 commits into from
May 31, 2015

Conversation

ahejlsberg
Copy link
Member

This PR implements support for local class, interface, enum, and type alias declarations. For example:

function f() {
    enum E {
        A, B, C
    }
    class C {
        x: E;
    }
    interface I {
        x: E;
    }
    type A = I[];
    let a: A = [new C()];
    a[0].x = E.B;
}

Local types are block scoped, similar to variables declared with let and const. For example:

function f() {
    if (true) {
        interface T { x: number }
        let v: T;
        v.x = 5;
    }
    else {
        interface T { x: string }
        let v: T;
        v.x = "hello";
    }
}

The inferred return type of a function may be a type declared locally within the function. It is not possible for callers of the function to reference such a local type, but it can of course be matched structurally. For example:

interface Point {
    x: number;
    y: number;
}

function getPointFactory(x: number, y: number) {
    class P {
        x = x;
        y = y;
    }
    return P;
}

var PointZero = getPointFactory(0, 0);
var PointOne = getPointFactory(1, 1);
var p1 = new PointZero();
var p2 = new PointZero();
var p3 = new PointOne();

Local types may reference enclosing type parameters and local class and interfaces may themselves be generic. For example:

function f1() {
    function f() {
        class C<X, Y> {
            constructor(public x: X, public y: Y) { }
        }
        return C;
    }
    let C = f();
    let v = new C(10, "hello");
    let x = v.x;  // number
    let y = v.y;  // string
}

function f2() {
    function f<X>(x: X) {
        class C<Y> {
            public x = x;
            constructor(public y: Y) { }
        }
        return C;
    }
    let C = f(10);
    let v = new C("hello");
    let x = v.x;  // number
    let y = v.y;  // string
}

function f3() {
    function f<X, Y>(x: X, y: Y) {
        class C {
            public x = x;
            public y = y;
        }
        return C;
    }
    let C = f(10, "hello");
    let v = new C();
    let x = v.x;  // number
    let y = v.y;  // string
}

Fixes #3217.
Fixes #2148.

// Type parameters of a function are in scope in the entire function declaration, including the parameter
// list and return type. However, local types are only in scope in the function body.
if (!(meaning & SymbolFlags.Type) || !(result.flags & (SymbolFlags.Type & ~SymbolFlags.TypeParameter)) ||
!isFunctionLike(location) || lastLocation === (<FunctionLikeDeclaration>location).body) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather this be split into more ifs than cram the conditions into one boolean expression.

Alternatively, break each subexpression onto its own line and explain each of them independently. For instance:

// We've found something in the 'locals' table, but we are sensitive to location in whether it is valid to use
// this entity.
//    - A non-type entity (i.e. values, namespaces, and aliases) is unconditionally resolved.
//    - A type parameters are visible throughout the entirety of a declaration, and are resolved.
//    - A local type found in any declaration *other than a function* can be considered resolved.
//    - A local types found in a function is only resolved if it was used *within the body of the function*.
if (!(meaning & SymbolFlags.Type) ||
    !(result.flags & (SymbolFlags.Type & ~SymbolFlags.TypeParameter)) ||
    !isFunctionLike(location) ||
    lastLocation === (<FunctionLikeDeclaration>location).body) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I think it is perfectly fine (and often preferable) to have multi-line boolean expressions instead of multiple if statements with "explaining variables". The latter just make the code even more complicated to look at. There's nothing inherently simpler about multiple if statements compared to multiple boolean operands separated by && and ||. Quite the opposite actually, since with boolean expressions you know there aren't any side effects or other oddities you have to worry about.

I do agree that in this case putting each condition on a separate line might make sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. I think that multiple if statements are actually simpler. They allow you to examine each clause, one at a time. They let each clause be simple explained. They do not require you to think through multiple different forms of boolean expressions in each clause.

Some minds (mine included) tend to have the problem where we see one expression, and then 'stamp' what we saw there on the next in a complicated clause. So if one expression does || and the next does &&, or one uses ! while the other does not, then it's very easy to 'trip' over the expression, forcing the need to go back and scan and understand the expression over and over again.

At this point, we've been using this code for around a year at this point, and there are still comments/complaints on the PRs about the clarity of the code. At this point, i think we should accept that these constructs are difficult for many team members to read, and we should err on the side of making the code as maintainable as possible for the team as a whole.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll just have to disagree here. If the team has problems reading multi-line boolean expressions then I think we have a bigger problem. You're advocating turning simple and functional boolean expressions into multiple imperative if statements with temporary variables. I think that is exactly the wrong way to go. Whenever I see those I always wonder if I'm missing something subtle (which imperative code is full of), because otherwise it would just be written as a simple boolean expression.

nextToken();
return token === SyntaxKind.EnumKeyword
function isStatement(): boolean {
return (getStatementFlags() & StatementFlags.Statement) !== 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about the purpose of the getStatementFlags approach. Why is it better than having isStatement and isModuleElement just make the decisions for themselves? Is it so that there can be just one place (instead of two) to account for all the node kinds here, so we don't forget any?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is modifiers. When modifiers are present we can't tell from the first token what we're looking at. So we need to look ahead, but we don't want to do that more than once. And, as you say, when everything is in one place we're certain that there won't be any subtle differences.

@JsonFreeman
Copy link
Contributor

In terms of the parser, I prefer the approach where we indiscriminately parse statements and module elements, and subsequently error in the checker if you have a module element in a block context. Since we may eventually allow module declarations inside functions, that work may prove useful to us anyway. But I defer to you, and otherwise, I sign off.

👍

@DanielRosenwasser
Copy link
Member

Since we've moved from typeParameters to innerTypeParameters, what is the interaction with signature help now?

For instance:

function f<FT>(x: FT) {
    class C<CT> {
        private y: CT;
    }

    return C;
}

let con = f(10);
let obj = new con</**/

Do we only see inner type params? This would be a good fourslash test.

@@ -3908,7 +4003,7 @@ module ts {
return mapper(<TypeParameter>type);
}
if (type.flags & TypeFlags.Anonymous) {
return type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) ?
return type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for anonymous class expressions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's for the anonymous type created for the static side of a class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, because previously the static side of a class could never get instantiated.

@JsonFreeman
Copy link
Contributor

Writing the types looks good 👍

@JsonFreeman
Copy link
Contributor

@DanielRosenwasser We do not have signature help for type arguments, only arguments. However, we do show the type arguments when we are giving sig help for the arguments, so this would be a good thing to test.

ahejlsberg added a commit that referenced this pull request May 31, 2015
@ahejlsberg ahejlsberg merged commit 0872ed6 into master May 31, 2015
@ahejlsberg ahejlsberg deleted the localTypes branch May 31, 2015 00:49
@danquirk danquirk mentioned this pull request Jul 23, 2015
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
7 participants