Skip to content

[@types/ember] rewrite computed property unwrapping to use infer#28282

Merged
minestarks merged 16 commits intoDefinitelyTyped:masterfrom
mike-north:new-cp-mapping
Sep 5, 2018
Merged

[@types/ember] rewrite computed property unwrapping to use infer#28282
minestarks merged 16 commits intoDefinitelyTyped:masterfrom
mike-north:new-cp-mapping

Conversation

@mike-north
Copy link
Contributor

@mike-north mike-north commented Aug 21, 2018

Please fill in this template.

  • Use a meaningful title for the pull request. Include the name of the package modified.
  • Test the change in your own code. (Compile and run.)
  • Add or edit tests to reflect the change. (Run with npm test.)
  • Follow the advice from the readme.
  • Avoid common mistakes.
  • Run npm run lint package-name (or tsc if no tslint.json is present).

A breaking change was introduced in recent typescript 3.1 nightly builds, likely due to changes in how arrays interact with mapping types. Unfortunately, @types/ember relied on the old behavior (removed by microsoft/TypeScript#26063) in order to infer the type of a computed property.

type ComputedPropertyGetters<T> = { [K in keyof T]: Ember.ComputedProperty<T[K], any> | ModuleComputed<T[K], any> | T[K] };
type ComputedPropertySetters<T> = { [K in keyof T]: Ember.ComputedProperty<any, T[K]> | ModuleComputed<any, T[K]> | T[K] };

(in the above example, imagine that T is an array, and you'll understand why our types for array.firstObject broke)

@dwickern suggested (and I agree) that we shift toward using infer for extracting the wrapped type, which unfortunately means dropping support for all TypeScript versions below 2.8 (this is consistent with our policy of supporting at least the latest two TS releases )

I'm deliberately not adding additional tests, to ensure that this fix takes care of the tests broken in master, and keeps the rest passing with no additional changes required.

Related: microsoft/TypeScript#26120
Fixes typed-ember/ember-cli-typescript#246
Fixes typed-ember/ember-cli-typescript#226

@typescript-bot
Copy link
Contributor

typescript-bot commented Aug 21, 2018

@mike-north Thank you for submitting this PR!

Because this is a new definition, a DefinitelyTyped maintainer will be reviewing this PR in the next few days once the Travis CI build passes.

In the meantime, if the build fails or a merge conflict occurs, I'll let you know. Have a nice day!

@typescript-bot
Copy link
Contributor

@mike-north Unfortunately, this pull request currently has a merge conflict 😥. Please update your PR branch to be up-to-date with respect to master. Have a nice day!

@typescript-bot typescript-bot added Has Merge Conflict This PR can't be merged because it has a merge conflict. The author needs to update it. and removed Has Merge Conflict This PR can't be merged because it has a merge conflict. The author needs to update it. labels Aug 24, 2018
@mike-north mike-north force-pushed the new-cp-mapping branch 4 times, most recently from ac295a0 to 5854354 Compare August 27, 2018 07:18
@typescript-bot typescript-bot added the New Definition This PR creates a new definition package. label Aug 27, 2018
@mike-north
Copy link
Contributor Author

mike-north commented Aug 27, 2018

To reviewers

Literally 99% lines changed in this pull request are due to cutting several major releases. The 1% that involves changes to types (as opposed to just copying files or breaking them into multiple files) is all in one commit. This is the actual bug fix. Everything else is to ensure we release this properly and don't break anyone's app.

0d8e5c1

There are two reasons this PR cannot be safely split up:

  1. Bumping up the minimum typescript version for @ember/types to 2.8 required doing the same to a bunch of other types that depend on it. A bunch of the people who maintain these types got together and agree that this fix should be considered a breaking change. At minimum we need to prevent developers on TS 2.4-2.7 from taking in incompatible code via a yarn upgrade.

  2. Creating a new ember-data major release clashed with some of the types-publisher scripts. ember-data/v2's mapping to ember/v2 was detected as an "unused mapping" (I'm guessing, due to the fact that imports were inside module declarations instead of in the top-level module scope).

declare module 'ember-data' {
   import Ember from 'ember'; // <-- this wasn't enough for ts-publisher
							  // 	 to detect ember/v2 as "used"
   export emberDataStuff;
}

The easiest way to solve this was breaking our single index.d.ts into multiple files

import Ember from 'ember'; // <-- ts-publisher was happy with this refactor

export emberDataStuff;

@dwickern
Copy link
Contributor

This seems to fix typed-ember/ember-cli-typescript#226 as a side effect. Maybe you could add that issue's reproduction to the test suite.

@mike-north
Copy link
Contributor Author

mike-north commented Aug 27, 2018

@dwickern I'd prefer to do that in a second pass, in case it's only partially fixed, or only fixed in a certain range of TS versions (i.e., 3+). Glad to hear that an extra issue may have accidentally been resolved!

Copy link
Contributor

@dfreeman dfreeman left a comment

Choose a reason for hiding this comment

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

Thanks for all your work fixing this and defining a versioning strategy! There are a couple of places where I think the change in strategy for unwrapping computeds means the signatures need to be reworked slightly, but overall this looks great.

static create<Instance, Args, T1 extends EmberInstanceArguments<Args>>(
this: EmberClassConstructor<Instance & ComputedPropertyGetters<Args>>,
this: EmberClassConstructor<UnwrapComputedPropertyGetters<Args> & Instance>,
arg1: T1 & ThisType<Fix<T1 & Instance>>
Copy link
Contributor

Choose a reason for hiding this comment

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

I never fully wrapped my head around how the previous approach was working here, but I think this is backwards now? It looks like the existing tests for create aren't super extensive, but given how the Getters wrapper moved from the source to the return value in e.g. the signature of get, I think I would expect a similar change here?

// Before
function get<T, K extends keyof T>(obj: ComputedPropertyGetters<T>, key: K): T[K];

// After
function get<T, K extends keyof T>(obj: T, key: K): UnwrapComputedPropertyGetter<T[K]>;

I.e. the instance type of the this value here can have ComputedProperty properties, but arg1 shouldn't, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

With TS 2.8 I think we can declare it more simply:

static create<Class extends typeof Ember.CoreObject, Arg1>(
    this: Class,
    arg1: Arg1 & ThisType<Arg1 & InstanceType<Class>>
): InstanceType<Class> & Arg1;

I'll call out a couple of things that are missing compared to the original declaration.

  • EmberInstanceArguments<T> verified that the arguments passed to create() matched the types on the class. E.g. Object.extend({ x: 42 }).create({ x: 'foo' }) would not typecheck (although it does work at runtime-- x is a string).

  • Fix<T> worked around Implement typed computed property access typed-ember/ember-typings#32. I suspect it's no longer needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be useful for us to take a pass at create and the associated code as a follow-on effort and see what we can clean up there (as well as fleshing out the test coverage).

In the meantime, though, hopefully there's a low-lift way to keep things working roughly as-is with UnwrapComputedPropertyGetters to get this PR landed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the future I also want us to stop &ing InstanceType<Class> & Arg1. I don't believe collisions are handled properly.

{ // InstanceType<Class>
   foo: string
} & { // .create args
   foo: number
} = {
  foo: string & number
}

it should be 

{
   foo: number
}

Maybe something like

type Mix<A, B> = B & Pick<A, Exclude<keyof A, keyof B>>;

example

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed that &ing together the types is incorrect there (and doubtless there are other places we're doing that as well that should be fixed), but shouldn't code like this be a type error regardless?

const MyClass = EmberObject.extend({
  foo: 'hello'
});
MyClass.create({ foo: 5 });

Seems like (roughly) the moral equivalent of

class MyClass {
  constructor(public foo: string = 'hello') {}
}
new MyClass(5);

Copy link
Contributor

Choose a reason for hiding this comment

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

@mike-north I'm not positive I'm in a better position than you are w.r.t. the current mechanics of create, but I'm happy to take a pass later today and see if I can get the tests passing if @dwickern doesn't beat me to it.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can add the EmberInstanceArguments constraint back to my previous example.

static create<
  Class extends typeof Ember.CoreObject,
  Arg1 extends EmberInstanceArguments<UnwrapComputedPropertySetters<InstanceType<Class>>>
>(
    this: Class,
    arg1: Arg1 & ThisType<Arg1 & InstanceType<Class>>
): InstanceType<Class> & Arg1;

Should Person.create({ fullName: 'string' }) be allowed?

Copy link
Contributor

@dfreeman dfreeman Aug 29, 2018

Choose a reason for hiding this comment

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

Unless it complicates the typing significantly, I believe it should, as long as the Set type on the computed is compatible. It's a little weird in this specific example since the computed isn't actually set up to work with set, but that's not currently something we try to capture in the type system (though authors could do that explicitly with Computed<string, never>)

Copy link
Contributor Author

@mike-north mike-north Aug 29, 2018

Choose a reason for hiding this comment

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

I'll give you both commit access to my fork. Unfortunately, I'm out of time to spend on this for a few days

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Ember.Object.create issue is now resolved. Positive and negative test cases added to reflect this for 0-4 arguments passed to .create

* This mixin provides properties and property observing functionality, core features of the Ember object model.
*/
interface Observable {
thisType: this;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this for? If it's needed, could you add a comment describing why?

obj: ComputedPropertyGetters<T>,
obj: UnwrapComputedPropertyGetters<T>,
key: K
): T[K] | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

UnwrapComputedPropertyGetters should be wrapping the return type here rather than obj, shouldn't it?

*/
function set<T, K extends keyof T, V extends T[K]>(
obj: ComputedPropertySetters<T>,
obj: UnwrapComputedPropertySetters<T>,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is another inversion.

function set<T, K extends keyof T, V extends UnwrapComputedPropertySetter<T[K]>>(
  obj: t,
  key: K,
  value: V
): V;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you guys contribute some test cases that validate the correct behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dfreeman I don't believe that this is correct. Current tests fail w/ your suggested implementation, and pass with the current code

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to work either way 🤷‍♂️

Would it be more correct for set to return the get type?

function set<T, K extends keyof T>(
    obj: T,
    key: K,
    value: UnwrapComputedPropertySetter<T[K]>
): UnwrapComputedPropertyGetter<T[K]>;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

^ this works

@typescript-bot
Copy link
Contributor

typescript-bot commented Aug 28, 2018

@mike-north The Travis CI build failed! Please review the logs for more information.

Once you've pushed the fixes, the build will automatically re-run. Thanks!

Copy link
Contributor

@dfreeman dfreeman left a comment

Choose a reason for hiding this comment

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

LGTM for the packages with my fingerprints on them. Thanks Mike!

hello() { return 'world'; }
protected bar() { return 'bar'; } // $ExpectError
private baz() { return 'baz'; }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

const p4 = new PersonWithNumberName();
// assertType<number>(p4.get('fullName'));

// assertType<Ember.ComputedProperty<string, string>>(p4.fullName); // $ExpectError
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a story on these commented type assertions? Based on the assertions in test/create.ts I'd expect p4.get('fullName') and p4.fullName both to have type number, but I'm not sure what the aim is here.

Copy link
Contributor Author

@mike-north mike-north Sep 4, 2018

Choose a reason for hiding this comment

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

The line you're commenting on is the interesting. Currently (and in prior releases of these types) the type of fullName is string && ComputedProperty<string, string>.

This is not an accurate representation of what's happening with Ember.Object.create and Ember.Object.extend -- these methods align with the behavior of Object.assign where target object properties are usually replaced.

In this case, the value should just be string. We'll resolve this when we apply the change described here as a future enhancement.

@typescript-bot typescript-bot added Owner Approved A listed owner of this package signed off on the pull request. Merge:Express and removed Awaiting reviewer feedback labels Sep 4, 2018
@typescript-bot
Copy link
Contributor

A definition owner has approved this PR ⭐️. A maintainer will merge this PR shortly. If it shouldn't be merged yet, please leave a comment saying so and we'll wait. Thank you for your contribution to DefinitelyTyped!

@minestarks minestarks merged commit 92553da into DefinitelyTyped:master Sep 5, 2018
@mike-north mike-north deleted the new-cp-mapping branch September 5, 2018 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

New Definition This PR creates a new definition package. Owner Approved A listed owner of this package signed off on the pull request.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[@types/ember bug] - ember types are broken in TypeScript 3.1 [Bug] Non-public access modifier keywords break lexical scope for property access

5 participants