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

[BUGFIX release] Correct types for Ember Arrays #20256

Merged
merged 2 commits into from
Nov 9, 2022

Conversation

wagenet
Copy link
Member

@wagenet wagenet commented Nov 7, 2022

This PR does a couple of things to improve array types:

  • More fully unifies internal array types with external preview types.
  • Corrects NativeArray types to not override methods from the true native JS array.

Fixes #20230

@wagenet wagenet requested a review from chriskrycho November 7, 2022 17:42
@wagenet wagenet force-pushed the native-array-fixes branch from 86ce43b to fa78d49 Compare November 7, 2022 18:09
@chriskrycho
Copy link
Contributor

Top-level request (I'll review further later): can you add an explanation to the PR so we can refer to it later to understand why these specific changes are correct?

@wagenet wagenet force-pushed the native-array-fixes branch 3 times, most recently from 5c8555b to cfaf5d5 Compare November 7, 2022 19:39
Comment on lines 3 to 13
export type MethodsOf<O> = {
[K in keyof O]: O[K] extends AnyFn ? O[K] : never;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't actually work, unfortunately. You need something like the final keyof as used in MethodNamesOf below to make it properly strip out the bits which are actually never. It turns out the easiest way to get MethodsOf is to reuse MethodNamesOf in conjunction with Pick:

type MethodsOf<O> = Pick<O, MethodNamesOf<O>>;

playground

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export type MethodsOf<O> = {
[K in keyof O]: O[K] extends AnyFn ? O[K] : never;
};
export type MethodsOf<O> = Pick<O, MethodNamesOf<O>>;

packages/@ember/array/index.ts Outdated Show resolved Hide resolved
packages/@ember/array/index.ts Outdated Show resolved Hide resolved
@wagenet wagenet force-pushed the native-array-fixes branch from 4513a24 to 3a1c614 Compare November 7, 2022 19:42
@@ -15,7 +15,7 @@ declare module '@ember/array/mutable' {
/**
* __Required.__ You must implement this method to apply this mixin.
*/
replace(idx: number, amt: number, objects: T[]): this;
replace(idx: number, amt: number, objects: T[]): void;
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 some cases we do return this, but the docs state that we do not return.

packages/@ember/array/index.ts Outdated Show resolved Hide resolved
@@ -535,7 +540,7 @@ interface EmberArray<T> extends Enumerable {
@return {Object} receiver
@public
*/
setEach<K extends string>(key: K, value: Value<T, K>): this;
setEach<K extends keyof T>(key: K, value: T[K]): this;
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 theory we can set compound keys, but I feel like we probably shouldn't.

find<S extends T, Target = void>(
predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
thisArg?: Target
): S | undefined;
Copy link
Member Author

Choose a reason for hiding this comment

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

Just moved this.

invoke<M extends MethodNamesOf<T>>(
methodName: M,
...args: MethodParams<T, M>
): NativeArray<MethodReturns<T, M>>;
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 public preview types were better here.

@@ -26,13 +27,11 @@ expectTypeOf(people.objectAt(0)).toEqualTypeOf<Person | undefined>();
expectTypeOf(people.objectsAt([1, 2, 3])).toEqualTypeOf<Array<Person | undefined>>();

expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf<Person[]>();
expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf<MutableArray<Person>>();
expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf<NativeArray<Person>>();
Copy link
Member Author

Choose a reason for hiding this comment

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

NativeArray !== MutableArray

@@ -153,7 +153,7 @@ expectTypeOf(Ember.Application.create()).toEqualTypeOf<Ember.Application>();
expectTypeOf(new Ember.ApplicationInstance()).toEqualTypeOf<Ember.ApplicationInstance>();
expectTypeOf(Ember.ApplicationInstance.create()).toEqualTypeOf<Ember.ApplicationInstance>();
// Ember.Array
const a1: Ember.Array<string> = Ember.A([]);
const a1: Ember.NativeArray<string> = Ember.A([]);
Copy link
Member Author

Choose a reason for hiding this comment

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

NativeArray !== EmberArray

* The final definition of NativeArray removes all native methods. This is the list of removed methods
* when run in Chrome 106.
*/
type IGNORED_MUTABLE_ARRAY_METHODS =
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 below should be the same as internal.

@@ -4,6 +4,9 @@ declare module '@ember/array' {
import Enumerable from '@ember/array/-private/enumerable';
import NativeArray from '@ember/array/-private/native-array';

// We don't currently attempt to fully type-check compound keys
type CompoundPropertyKey<T> = `${keyof T & string}.${string}`;
Copy link
Member Author

Choose a reason for hiding this comment

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

Bring these more in line with internal types.

@@ -45,12 +45,12 @@ declare module '@ember/array/mutable' {
* Pop object from array or nil if none are left. Works just like `pop()` but
* it is KVO-compliant.
*/
popObject(): T;
popObject(): T | null | undefined;
Copy link
Member Author

Choose a reason for hiding this comment

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

These types were incorrect before.

@wagenet wagenet force-pushed the native-array-fixes branch 2 times, most recently from a6735ca to e3d387e Compare November 7, 2022 21:25
@@ -792,6 +790,10 @@ interface EmberArray<T> extends Enumerable {
@return {Object} Found item or `undefined`.
@public
*/
find<S extends T, Target = void>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: conventionally/idiomatically it's U in a case like this (since T → U in the alphabet). Not a thing I care about changing, to be sure, just worth noting!

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I just copied this from somewhere else :)

Comment on lines +1904 to +1907
/**
* The final definition of NativeArray removes all native methods. This is the list of removed methods
* when run in Chrome 106.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Note for posterity/future readers looking at this PR historically—we definitely need this, but we need even more to just get the heck rid of it, since this could in principle change dynamically over time.


expectTypeOf(arr).toMatchTypeOf<MutableArray<Foo>>();

expectTypeOf(arr.replace(1, 1, [foo])).toEqualTypeOf<void>();
// TODO: Why doesn't this fail?
// @ts-expect-error invalid item
Copy link
Contributor

Choose a reason for hiding this comment

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

It's nice that all of these fail now the way we want them to. 👍🏼

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we just needed a better test item :)

Copy link
Contributor

@chriskrycho chriskrycho left a comment

Choose a reason for hiding this comment

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

Couple of tweaks needed, but this is a really nice step forward. 💙

Comment on lines 89 to 91
catalogEntriesByType(type: string): string[] {
let namespaces = Namespace.NAMESPACES;
let types: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. Documentation is very apt to be incorrect in this space, though; folks who wrote the docs likely were not thinking about the differences (because the differences are subtle!). Let's avoid changing the behavior here—we can always do so in a later PR, but keeping types changes separate from behavior changes will help us avoid snafus.

let containerDebugAdapter = this.containerDebugAdapter;

let stringTypes = containerDebugAdapter.canCatalogEntriesByType('model')
? containerDebugAdapter.catalogEntriesByType('model')
: this._getObjectsOnNamespaces();

// New adapters return strings instead of classes.
let klassTypes = emberA(stringTypes).map((name) => {
let klassTypes = stringTypes.map((name) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

On the one hand, this is definitely a change I'm excited to be able to make; on the other, my note here is the same as above. Let's do it in a separate PR so it's easy to trace back to if for some reason it ends up being an issue for folks.

Comment on lines 574 to 575
let namespaces = Namespace.NAMESPACES;
let types: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, presumably.

@chriskrycho chriskrycho changed the title Improve types for Ember Arrays [BUGFIX release] Correct types for Ember Arrays Nov 9, 2022
@chriskrycho chriskrycho added Bug TypeScript Work on Ember’s types labels Nov 9, 2022
1. Using the keyof constraint feature introduced in 4.1, avoid ever
   introducing types with properties whose type is `never` for the
   non-method fields.

2. Handle recursively applying this constraint to nested methods for
   the `EmberMethodsOf` type, which has additional complexity to handle
   Ember's string-based dispatch. Combined with (1), this actually
   *simplifies* the types, eliminating one layer of conditional types.
@chriskrycho chriskrycho enabled auto-merge November 9, 2022 20:11
@chriskrycho chriskrycho merged commit fae849c into emberjs:master Nov 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug TypeScript Work on Ember’s types
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Preview Types Bug] map on an EmberArray returns EmberArray but shouldn't
2 participants