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

Contextually type implemented properties #6118

Closed
wants to merge 19 commits into from

Conversation

sandersn
Copy link
Member

Fixes #3667. Fixes #1373.

interface I {
  f(n: number): string;
  p: string;
}
class C implements I {
  f = n => n.toString(); // n is correctly typed as `number`
  p = undefined; // correctly retains type `string`
}

Interesting baseline changes in the latest commit:

let x: number;
let y: string;
x = y = undefined;

is now illegal. This is a breaking change.

let [...r] = null

This is now legal because null gets contextually typed as an array, although node says "Cannot read property 'Symbol(Symbol.iterator)' of null" when running the code.

There are also a couple of implicit any errors that went missing from wideningTuples3 and wideningTuples5. I still need to find out why the error no longer fires:

var a: [any];
var b = a = [undefined, null];

Should have an an error when b implicitly gets the type [any, any].

This also applies when the assigned value would be widened to a type other
than the contextually inherited type. For example, an inherited property
`p: number` now correctly maintains its type even when initialised with
`p = undefined` in a subclass. Previously it just widened to `any`.
Previously, the implemented property `i` took the type of its
initialiser (`i = "foo"`). Now it takes the type of the property it inherits
(`base1.i : any`).
@@ -2895,6 +2902,34 @@ namespace ts {
return unknownType;
}

function getTypeOfBasePropertyDeclaration(declaration: VariableLikeDeclaration) {
Copy link
Member

Choose a reason for hiding this comment

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

change to declaration: PropertyDeclaration.

Also:
1. Address review comments.
2. Add test cases to implementedPropertyContextualTyping.*
3. Improve some tests: bestCommonTypeOfTuple2 and
destructuringParameterDeclaration2.
4. Make some tests worse due to exposing bug 6190:
requiredInitializedParameter.*. I'll fix it separately.
if (declaration.kind === SyntaxKind.PropertyDeclaration) {
const type = getTypeOfBasePropertyDeclaration(<PropertyDeclaration>declaration);
if (type) {
mapper = createTypeMapper([undefinedType, nullType], [type, type]);
Copy link
Member

Choose a reason for hiding this comment

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

Since the first argument never changes, you could factor it out to a constant.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not so sure this works - doesn't this map all undefined and null types in the expression? So if I have

class Base {
    a: (x: string) => void;
}

class Derived extends Base {
    a = (x = null) => {}
}

won't x in Derived get the type (x: string) => void meaning a will have the type (x: (x: string) => void) => void.

Maybe I misunderstood.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Also, I think the TypeMappers are supposed to be for generics.

@sandersn
Copy link
Member Author

sandersn commented Jan 7, 2016

Yes, Anders explained contextual typing in person and I'll push new changes with getContextualType as soon as I make sure that all the baseline changes are correct.

Inside other getContextual* functions.
This requires checking that getting contextual type is not in an
inferential context.
@sandersn
Copy link
Member Author

sandersn commented Jan 7, 2016

Here's why wideningTuples3 fails to report a noImplicitAny error:

var a: any[];
var b = a = [undefined, null];

a contextually types [undefined, null]: any[], which in turn contextually types undefined and null as any. Then b infers the type [any, any] since it has an array of two elements, each of type any.

But this doesn't trigger the implicit any check in reportErrorsFromWidening because it looks for types that will be widened to any -- undefined or null. But these particular undefined/null values have already been assigned the type any. It's arguably no longer implicit since their types were contextually propagated from the explicit var a: any[].

I'm not sure whether this needs to be fixed, or whether it's an obscure enough corner case to let the technicality slide.

@sandersn
Copy link
Member Author

sandersn commented Jan 8, 2016

After discussing with Mohamed, I think dropping the implicit any error is the right thing in this case.

@RyanCavanaugh
Copy link
Member

Observable types from contextually-typed expressions have always been very weird, so I don't think it's much to worry about. I think that code has changed meaning more than once since 1.0.

@JsonFreeman
Copy link
Contributor

I agree with @mhegazy and @sandersn about dropping the no implicit any error.

@DanielRosenwasser
Copy link
Member

Remind me, does this also fix #1373?

1. Contextual typing of inherited properties with string literal types.  This required no changes.
2. With strict-null, null/undefined should only get contextual types that include null / undefined.
3. Conflicting inherited properties from interfaces should not contextually type the implementation.

Note that for (3), properties inherited from classes still contextually type the implementation,
even if there are later conflicts with properties inherited from interfaces.
Use the contextual type from conflicting properties inherited from interfaces only if the types are equal.
Previously these properties were not contextually typed.
@sandersn
Copy link
Member Author

sandersn commented May 3, 2016

Running on real world code

We discussed this PR in the design meeting last week and decided that it was too inconsistent and didn't capture intent well enough. Instead, I tried a simpler approach that gives inherited properties exactly their inherited type (unless there is a type annotation). The change is found in the branch contextually-type-inherited-properties-WIP. I ran this branch on our real world code base consisting of Microsoft internal code as well as projects like Angular and Grunt. This showed that the new change broke more than it helped. Since both approaches failed, I am going to close this PR -- we may revisit this problem in the future.

The good

The improved typing exposed one bug that happens twice in the same project:

interface I {
  method(o: SomeType): string;
}
class C extends I {
  method(o) {
    return o.doesNotExist;
  }
}

Before this change, o was implicitly any in the parameter list of C#method.
Now an error is reported because o: SomeType, which does not have a
property named doesNotExist.

Both methods would have caught this bug. So would --noImplicitAny.

The bad

Consider the following example:

namespace Base {
    export interface Item {
        a;
    }
    export abstract class Base {
        abstract item: Item;
    }
}
namespace User {
    export interface Subitem extends Base.Item {
        sub;
    }
    export interface Contract {
        item: Subitem;
    }
    export function createC(): Contract {
    }
    function createSubitem(): Subitem {
        // factory code here ...
    }
    class C extends Base.Base implements Contract {
        item = createSubitem();
    }
}

The intended usage is something like

let c: User.Contract = User.createC();
console.log(c.item.a);
console.log(c.item.sub);

Unfortunately, if C.item takes its type from Base.item, then
C.item: Item. Then C no longer implements Contract because
Contract.item: Subitem.

The ugly

In several projects, properties of type KnockoutObservable changed to
KnockoutObservableBase, which is a superclass that has fewer
properties. This can be fixed by upgrading to a non-ancient version of
knockout.d.ts, though. Current versions of knockout.d.ts don't have
KnockoutObservableBase.

@sandersn sandersn closed this May 3, 2016
@DanielRosenwasser
Copy link
Member

👍 Thanks for reporting on what sorts of problems/improvements this brought.

@tinganho
Copy link
Contributor

tinganho commented May 6, 2016

if A extends B and implements C. Why doesn't it just contextually type based on members of B and C and when there exists common members contextually type based on the most sub subtype member of B and C?

Wouldn't that remove the bad and ugly part?

@JsonFreeman
Copy link
Contributor

@tinganho I believe that in general there is not a lowest subtype for a set of types. The type system does not provide a total ordering.

@mhegazy mhegazy deleted the contextually-type-implemented-properties branch November 2, 2017 21:05
@DanielRosenwasser
Copy link
Member

Keywords: base derived methods properties property contextual contextually type subclass superclass

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants