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

Control flow based type analysis #8010

Merged
merged 70 commits into from Apr 22, 2016

Conversation

Projects
None yet
@ahejlsberg
Member

ahejlsberg commented Apr 11, 2016

This PR introduces control flow based type analysis for local variables and parameters as initially suggested in #2388 and prototyped in #6959. Previously, the type analysis performed for type guards was limited to if statements and ?: conditional expressions and didn't include effects of assignments and control flow constructs such as return and break statements. With this PR, the type checker analyses all possible flows of control in statements and expressions to produce the most specific type possible (the narrowed type) at any given location for a local variable or parameter that is declared to have a union type.

Some examples:

function foo(x: string | number | boolean) {
    if (typeof x === "string") {
        x; // type of x is string here
        x = 1;
        x; // type of x is number here
    }
    x; // type of x is number | boolean here
}

function bar(x: string | number) {
    if (typeof x === "number") {
        return;
    }
    x; // type of x is string here
}

Control flow based type analysis is particuarly relevant in --strictNullChecks mode because nullable types are represented using union types:

function test(x: string | null) {
    if (x === null) {
        return;
    }
    x; // type of x is string in remainder of function
}

Furthermore, in --strictNullChecks mode, control flow based type analysis includes definite assignment analysis for local variables of types that don't permit the value undefined.

function mumble(check: boolean) {
    let x: number; // Type doesn't permit undefined
    x; // Error, x is undefined
    if (check) {
        x = 1;
        x; // Ok
    }
    x; // Error, x is possibly undefined
    x = 2;
    x; // Ok
}

The narrowed type of a local variable or parameter at a given source code location is computed by starting with the initial type of the variable and then following each possible code path that leads to the given location, narrowing the type of the variable as appropriate based on type guards and assignments.

  • The initial type of local variable is undefined.
  • The initial type of a parameter is the declared type of the parameter.
  • The initial type of an outer local variable or a global variable is the declared type of that variable.
  • A type guard narrows the type of a variable in the code path that follows the type guard.
  • An assignment (including an initializer in a declaration) of a value of type S to a variable of type T changes the type of that variable to T narrowed by S in the code path that follows the assignment.
  • When multiple code paths lead to a particular location, the narrowed type of a given variable at that location is the union type of the narrowed types of the variable in those code paths.

The type T narrowed by S is computed as follows:

  • If T is not a union type, the result is T.
  • If T is a union type, the result is the union of each constituent type in T to which S is assignable.

Thanks to @ivogabe for providing inspiration and tests for this PR.

Fixes #2388.

ahejlsberg and others added some commits Mar 22, 2016

Merge pull request #7690 from ivogabe/controlFlowTypesTest
Adds tests to control flow types branch
Merge branch 'master' into controlFlowTypes
Conflicts:
	src/compiler/checker.ts
	tests/baselines/reference/typeAssertions.errors.txt
function isNarrowableReference(expr: Expression): boolean {
return expr.kind === SyntaxKind.Identifier ||
expr.kind === SyntaxKind.ThisKeyword ||
expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((<PropertyAccessExpression>expr).expression);

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

Should ElementAccessExpressions be considered narrowable if their expression is narrowable, just like PropertyAccessExpressions?

@weswigham

weswigham Apr 11, 2016

Member

Should ElementAccessExpressions be considered narrowable if their expression is narrowable, just like PropertyAccessExpressions?

This comment has been minimized.

@ivogabe

ivogabe Apr 11, 2016

Contributor

That could be done, but only if the expression between the brackets is a constant (string/number literal, enum value etc)

@ivogabe

ivogabe Apr 11, 2016

Contributor

That could be done, but only if the expression between the brackets is a constant (string/number literal, enum value etc)

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

I think we do that kind of special-casing for literal element access expressions elsewhere - we should probably do it here, as well.

@weswigham

weswigham Apr 11, 2016

Member

I think we do that kind of special-casing for literal element access expressions elsewhere - we should probably do it here, as well.

This comment has been minimized.

@mhegazy

mhegazy Apr 15, 2016

Contributor

literals and well-known symbols as well.

@mhegazy

mhegazy Apr 15, 2016

Contributor

literals and well-known symbols as well.

This comment has been minimized.

@ahejlsberg

ahejlsberg Apr 15, 2016

Member

Yes, it would be good to support element access expressions with string literals and well known symbols. I think we can cover it in a separate PR though.

@ahejlsberg

ahejlsberg Apr 15, 2016

Member

Yes, it would be good to support element access expressions with string literals and well known symbols. I think we can cover it in a separate PR though.

return flowTypeCaches[flow.id] || (flowTypeCaches[flow.id] = {});
}
function isNarrowableReference(expr: Node): boolean {

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

This function is duplicated both here and in the binder. Should it be shared?

@weswigham

weswigham Apr 11, 2016

Member

This function is duplicated both here and in the binder. Should it be shared?

@@ -1504,7 +1529,7 @@ namespace ts {
}
// True if the given identifier, string literal, or number literal is the name of a declaration node
export function isDeclarationName(name: Node): name is Identifier | StringLiteral | LiteralExpression {
export function isDeclarationName(name: Node): boolean {

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

Why was the type-guardiness of this function removed?

@weswigham

weswigham Apr 11, 2016

Member

Why was the type-guardiness of this function removed?

This comment has been minimized.

@ivogabe

ivogabe Apr 11, 2016

Contributor

Probably because this function can also return true if the node is some identifier, but not used as a declaration. With the control flow checks, the code after an if with a return will be considered to be the else block, which might have caused some issues.

@ivogabe

ivogabe Apr 11, 2016

Contributor

Probably because this function can also return true if the node is some identifier, but not used as a declaration. With the control flow checks, the code after an if with a return will be considered to be the else block, which might have caused some issues.

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

If that was the case, then wouldn't the name of the function be misleading?

@weswigham

weswigham Apr 11, 2016

Member

If that was the case, then wouldn't the name of the function be misleading?

This comment has been minimized.

@ivogabe

ivogabe Apr 11, 2016

Contributor

Not in my opinion, it checks whether a node is a declaration name, but not every identifier is a declaration name. So the type annotation was wrong, not the name.

@ivogabe

ivogabe Apr 11, 2016

Contributor

Not in my opinion, it checks whether a node is a declaration name, but not every identifier is a declaration name. So the type annotation was wrong, not the name.

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

Seems fair.

@weswigham

weswigham Apr 11, 2016

Member

Seems fair.

This comment has been minimized.

@ahejlsberg

ahejlsberg Apr 11, 2016

Member

Yes, @ivogabe is exactly right about why I removed the type predicate annotation.

@ahejlsberg

ahejlsberg Apr 11, 2016

Member

Yes, @ivogabe is exactly right about why I removed the type predicate annotation.

@@ -1811,7 +1811,7 @@ namespace ts {
function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: DiagnosticMessage): EntityName {
let entity: EntityName = parseIdentifier(diagnosticMessage);
while (parseOptional(SyntaxKind.DotToken)) {
const node = <QualifiedName>createNode(SyntaxKind.QualifiedName, entity.pos);
const node: QualifiedName = <QualifiedName>createNode(SyntaxKind.QualifiedName, entity.pos); // !!!

This comment has been minimized.

@weswigham

weswigham Apr 11, 2016

Member

What's the new type annotation and // !!! comment for?

@weswigham

weswigham Apr 11, 2016

Member

What's the new type annotation and // !!! comment for?

This comment has been minimized.

@ivogabe

ivogabe Apr 11, 2016

Contributor

Since Entity is a type alias of a union, it will be narrowed after its assignment. This creates a circular dependency during the type analysis. Because of this, node would be typed as any if the type annotation was not given.

@ahejlsberg Would it be possible to give the inference of the type of the initializer precedence over the narrowing after assignments?

@ivogabe

ivogabe Apr 11, 2016

Contributor

Since Entity is a type alias of a union, it will be narrowed after its assignment. This creates a circular dependency during the type analysis. Because of this, node would be typed as any if the type annotation was not given.

@ahejlsberg Would it be possible to give the inference of the type of the initializer precedence over the narrowing after assignments?

This comment has been minimized.

@ahejlsberg

ahejlsberg Apr 11, 2016

Member

The exact problem here is the following: We infer the type of node from the call to createNode. To evaluate that call we need to know the type of entity so we can find the type of the pos property. To find the type of entity we look at the preceding code paths. One leads from the top of the function. In that code path, entity has type Identifier, which is less than its full declared type (Identifier | QualifiedName). Therefore we need to also analyze the second code path that comes from the bottom of the while statement (the loop around case). In that code path, we have the assignment entity = finishNode(node) which requires us to know the type of node. Boom! Circularity. Not exactly clear what rule we'd introduce to break that circularity.

@ahejlsberg

ahejlsberg Apr 11, 2016

Member

The exact problem here is the following: We infer the type of node from the call to createNode. To evaluate that call we need to know the type of entity so we can find the type of the pos property. To find the type of entity we look at the preceding code paths. One leads from the top of the function. In that code path, entity has type Identifier, which is less than its full declared type (Identifier | QualifiedName). Therefore we need to also analyze the second code path that comes from the bottom of the while statement (the loop around case). In that code path, we have the assignment entity = finishNode(node) which requires us to know the type of node. Boom! Circularity. Not exactly clear what rule we'd introduce to break that circularity.

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Apr 12, 2016

Member

Would just assuming that entity has its original declared type be good enough?

@DanielRosenwasser

DanielRosenwasser Apr 12, 2016

Member

Would just assuming that entity has its original declared type be good enough?

This comment has been minimized.

@JsonFreeman

JsonFreeman Apr 12, 2016

Contributor

One other idea is that while doing the analysis, if you hit a type assertion, ignore the RHS, and just use the type associated with the type assertion.

@JsonFreeman

JsonFreeman Apr 12, 2016

Contributor

One other idea is that while doing the analysis, if you hit a type assertion, ignore the RHS, and just use the type associated with the type assertion.

This comment has been minimized.

@JsonFreeman

JsonFreeman Apr 13, 2016

Contributor

Yes that's a good point. It's not a simple matter of using the declared type. But I think the overloaded next function wouldn't even accept a union type as an argument.

@JsonFreeman

JsonFreeman Apr 13, 2016

Contributor

Yes that's a good point. It's not a simple matter of using the declared type. But I think the overloaded next function wouldn't even accept a union type as an argument.

This comment has been minimized.

@weswigham

weswigham Apr 13, 2016

Member

No, it wouldn't with how TS resolves signatures at present - at least not without another signature declaring it as taking a parameter of type any or a union of the other types. (Which, for some reason feels off to me - if every member of a union of argument types can be fulfilled by an overload and there's no better match, why can't the return be the union of those overloads' return values? Question for another time.)

@weswigham

weswigham Apr 13, 2016

Member

No, it wouldn't with how TS resolves signatures at present - at least not without another signature declaring it as taking a parameter of type any or a union of the other types. (Which, for some reason feels off to me - if every member of a union of argument types can be fulfilled by an overload and there's no better match, why can't the return be the union of those overloads' return values? Question for another time.)

This comment has been minimized.

@JsonFreeman

JsonFreeman Apr 13, 2016

Contributor

(Answer for another time ;)

@JsonFreeman

JsonFreeman Apr 13, 2016

Contributor

(Answer for another time ;)

This comment has been minimized.

@ahejlsberg

ahejlsberg Apr 13, 2016

Member

@weswigham @JsonFreeman There are actually several issues being debated here:

  • Circularities occurring because we're more eager than we need to when resolving (as opposed to checking) the type of an expression. As Jason points out, that's not a new issue. We could make progress on this issue by making a distinction between resolving and checking an expression, and only evaluate as much as we need to when resolving. Indeed, we could solve the actual cases in the compiler by doing this for type assertion expressions.
  • Inability for symbols to have evolving declared types. This one is much harder. I really don't want to go there unless we absolutely have to.
  • Compute narrowed types by iterating to a fixed point. We only do this in limited form today and, as Wes' example shows, we may need to do more here. I'm not super concerned with crazy examples that walk up a ladder of overloads, but I can indeed imagine real world examples where it occurs without overloading. Although I haven't actually seen any in the wild yet. I will continue to think about this issue.
@ahejlsberg

ahejlsberg Apr 13, 2016

Member

@weswigham @JsonFreeman There are actually several issues being debated here:

  • Circularities occurring because we're more eager than we need to when resolving (as opposed to checking) the type of an expression. As Jason points out, that's not a new issue. We could make progress on this issue by making a distinction between resolving and checking an expression, and only evaluate as much as we need to when resolving. Indeed, we could solve the actual cases in the compiler by doing this for type assertion expressions.
  • Inability for symbols to have evolving declared types. This one is much harder. I really don't want to go there unless we absolutely have to.
  • Compute narrowed types by iterating to a fixed point. We only do this in limited form today and, as Wes' example shows, we may need to do more here. I'm not super concerned with crazy examples that walk up a ladder of overloads, but I can indeed imagine real world examples where it occurs without overloading. Although I haven't actually seen any in the wild yet. I will continue to think about this issue.

This comment has been minimized.

@JsonFreeman

JsonFreeman Apr 13, 2016

Contributor

Thanks Anders, I think that's a great breakdown. I'll add that (if I'm not mistaken), this separation of resolution and checking is in place for bodies of function expressions. The idea here would be to generalize it for other expressions.

I agree that iterating to a fixed point may be necessary for loops in which the variable is reassigned in the loop body.

@JsonFreeman

JsonFreeman Apr 13, 2016

Contributor

Thanks Anders, I think that's a great breakdown. I'll add that (if I'm not mistaken), this separation of resolution and checking is in place for bodies of function expressions. The idea here would be to generalize it for other expressions.

I agree that iterating to a fixed point may be necessary for loops in which the variable is reassigned in the loop body.

@ahejlsberg ahejlsberg merged commit 5ed6f30 into master Apr 22, 2016

2 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details

basarat added a commit to alm-tools/alm that referenced this pull request Apr 22, 2016

@mhegazy mhegazy referenced this pull request Apr 22, 2016

Closed

Control flow based type guards #6959

5 of 5 tasks complete

@mquandalle mquandalle referenced this pull request Apr 23, 2016

Closed

WIP: Member-type based type guards #6062

0 of 4 tasks complete

dokidokivisual added a commit to karen-irc/karen that referenced this pull request May 4, 2016

Auto merge of #604 - saneyuki:nullable, r=saneyuki
chore(TypeScript): Enable 'strictNullChecks' option

This tries to enable [`--strictNullChecks` option](Microsoft/TypeScript#7140) of TypeScript compiler.

- [Non-nullable types by ahejlsberg · Pull Request #7140 · Microsoft/TypeScript](Microsoft/TypeScript#7140)
  - [Non-strict type checking · Issue #7489 · Microsoft/TypeScript](Microsoft/TypeScript#7489)
  - [[Request for feedback] Nullable types, `null` and `undefined` · Issue #7426 · Microsoft/TypeScript](Microsoft/TypeScript#7426)
- [Control flow based type analysis by ahejlsberg · Pull Request #8010 · Microsoft/TypeScript](Microsoft/TypeScript#8010)

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="35" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/karen-irc/karen/604)
<!-- Reviewable:end -->
@kristian-puccio

This comment has been minimized.

Show comment
Hide comment
@kristian-puccio

kristian-puccio May 16, 2016

Just trying out typescript now and working out for to get type check actions in redux. Looks like this feature could be very helpful for this?

reduxjs/redux#992

Just trying out typescript now and working out for to get type check actions in redux. Looks like this feature could be very helpful for this?

reduxjs/redux#992

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.