diff --git a/packages/@ember/-internals/glimmer/lib/utils/iterator.ts b/packages/@ember/-internals/glimmer/lib/utils/iterator.ts index 6cb3d1d5dd5..3a8c8d347ad 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/iterator.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/iterator.ts @@ -5,6 +5,7 @@ import type { Option } from '@glimmer/interfaces'; import type { IteratorDelegate } from '@glimmer/reference'; import { consumeTag, isTracking, tagFor } from '@glimmer/validator'; import { EachInWrapper } from '../helpers/each-in'; +import type { NativeArray } from '@ember/array'; export default function toIterator(iterable: unknown): Option { if (iterable instanceof EachInWrapper) { @@ -100,11 +101,11 @@ class ArrayIterator extends BoundedIterator { } class EmberArrayIterator extends BoundedIterator { - static from(iterable: EmberArray) { + static from(iterable: EmberArray | NativeArray) { return iterable.length > 0 ? new this(iterable) : null; } - constructor(private array: EmberArray) { + constructor(private array: EmberArray | NativeArray) { super(array.length); } diff --git a/packages/@ember/-internals/utils/lib/ember-array.ts b/packages/@ember/-internals/utils/lib/ember-array.ts index aadea305ec7..3c3d2bc2deb 100644 --- a/packages/@ember/-internals/utils/lib/ember-array.ts +++ b/packages/@ember/-internals/utils/lib/ember-array.ts @@ -1,4 +1,4 @@ -import type EmberArray from '@ember/array'; +import type { EmberArrayLike } from '@ember/array'; import { _WeakSet } from '@glimmer/util'; const EMBER_ARRAYS = new _WeakSet(); @@ -7,6 +7,6 @@ export function setEmberArray(obj: object) { EMBER_ARRAYS.add(obj); } -export function isEmberArray(obj: unknown): obj is EmberArray { +export function isEmberArray(obj: unknown): obj is EmberArrayLike { return EMBER_ARRAYS.has(obj as object); } diff --git a/packages/@ember/-internals/utils/types.ts b/packages/@ember/-internals/utils/types.ts index 461c513d5dc..2976e1f4e16 100644 --- a/packages/@ember/-internals/utils/types.ts +++ b/packages/@ember/-internals/utils/types.ts @@ -1,7 +1,15 @@ export type AnyFn = (...args: any[]) => any; +export type MethodsOf = { + [K in keyof O]: O[K] extends AnyFn ? O[K] : never; +}; + export type MethodNamesOf = { [K in keyof T]: T[K] extends AnyFn ? K : never; }[keyof T]; +export type MethodParams> = Parameters[M]>; + +export type MethodReturns> = ReturnType[M]>; + export type OmitFirst = F extends [any, ...infer R] ? R : []; diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index a5a57064f72..7afb5828020 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -20,12 +20,15 @@ import MutableEnumerable from '@ember/enumerable/mutable'; import { compare, typeOf } from '@ember/utils'; import { ENV } from '@ember/-internals/environment'; import Observable from '@ember/object/observable'; -import type { AnyFn } from '@ember/-internals/utils/types'; +import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utils/types'; import type { ComputedPropertyCallback } from '@ember/-internals/metal'; export { makeArray } from '@ember/-internals/utils'; -type Value = K extends keyof T ? T[K] : unknown; +export type EmberArrayLike = EmberArray | NativeArray; + +// We don't currently attempt to fully type-check compound keys +type CompoundPropertyKey = `${keyof T & string}.${string}`; const EMPTY_ARRAY = Object.freeze([] as const); @@ -319,7 +322,8 @@ interface EmberArray extends Enumerable { @return this @public */ - '[]': this; + get '[]'(): this; + set '[]'(newValue: T[] | EmberArray); /** The first object in the array, or `undefined` if the array is empty. @@ -515,7 +519,8 @@ interface EmberArray extends Enumerable { @return {Array} The mapped array. @public */ - getEach(key: K): NativeArray>; + getEach(key: K): NativeArray; + getEach>(key: K): NativeArray; /** Sets the value on the named property for each member. This is more ergonomic than using other methods defined on this helper. If the object @@ -535,7 +540,7 @@ interface EmberArray extends Enumerable { @return {Object} receiver @public */ - setEach(key: K, value: Value): this; + setEach(key: K, value: T[K]): this; /** Maps all of the items in the enumeration to another value, returning a new array. This method corresponds to `map()` defined in JavaScript 1.6. @@ -593,7 +598,8 @@ interface EmberArray extends Enumerable { @return {Array} The mapped array. @public */ - mapBy(key: K): NativeArray>; + mapBy(key: K): NativeArray; + mapBy>(key: K): NativeArray; /** Returns a new array with all of the items in the enumeration that the provided callback function returns true for. This method corresponds to [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). @@ -746,10 +752,6 @@ interface EmberArray extends Enumerable { @public */ rejectBy(key: string, value?: unknown): NativeArray; - find( - predicate: (this: void, value: T, index: number, obj: T[]) => value is S, - thisArg?: Target - ): S | undefined; /** Returns the first item in the array for which the callback returns true. This method is similar to the `find()` method defined in ECMAScript 2015. @@ -792,6 +794,10 @@ interface EmberArray extends Enumerable { @return {Object} Found item or `undefined`. @public */ + find( + predicate: (this: void, value: T, index: number, obj: T[]) => value is S, + thisArg?: Target + ): S | undefined; find( callback: (this: Target, item: T, index: number, arr: this) => unknown, target?: Target @@ -824,7 +830,8 @@ interface EmberArray extends Enumerable { @return {Object} found item or `undefined` @public */ - findBy(key: K, value?: Value): T | undefined; + findBy(key: K, value?: T[K]): T | undefined; + findBy>(key: K, value?: unknown): T | undefined; /** Returns `true` if the passed function returns true for every item in the enumeration. This corresponds with the `Array.prototype.every()` method defined in ES5. @@ -904,7 +911,8 @@ interface EmberArray extends Enumerable { @since 1.3.0 @public */ - isEvery(key: K, value?: Value): boolean; + isEvery(key: K, value?: T[K]): boolean; + isEvery>(key: K, value?: unknown): boolean; /** The any() method executes the callback function once for each element present in the array until it finds the one where callback returns a truthy @@ -972,7 +980,8 @@ interface EmberArray extends Enumerable { @since 1.3.0 @public */ - isAny(key: K, value?: Value): boolean; + isAny(key: K, value?: T[K]): boolean; + isAny>(key: K, value?: unknown): boolean; /** This will combine the values of the array into a single value. It is a useful way to collect a summary value from an array. This @@ -1061,10 +1070,10 @@ interface EmberArray extends Enumerable { @return {Array} return values from calling invoke. @public */ - invoke( - methodName: K, - ...args: Value extends AnyFn ? Parameters> : unknown[] - ): NativeArray extends AnyFn ? ReturnType> : unknown>; + invoke>( + methodName: M, + ...args: MethodParams + ): NativeArray>; /** Simply converts the object into a genuine array. The order is not guaranteed. Corresponds to the method implemented by Prototype. @@ -1086,7 +1095,7 @@ interface EmberArray extends Enumerable { @return {Array} the array without null and undefined elements. @public */ - compact(): NativeArray>; + compact(): NativeArray>; /** Used to determine if the array contains the passed object. Returns `true` if found, `false` otherwise. @@ -1176,6 +1185,7 @@ interface EmberArray extends Enumerable { @public */ uniqBy(key: string): NativeArray; + uniqBy(callback: (value: T) => unknown): NativeArray; /** Returns a new array that excludes the passed value. The default implementation returns an array regardless of the receiver type. @@ -1567,7 +1577,7 @@ interface MutableArray extends EmberArray, MutableEnumerable { @return object same object passed as a param @public */ - pushObject(obj: T): this; + pushObject(obj: T): T; /** Add the objects in the passed array to the end of the array. Defers notifying observers of the change until all objects are added. @@ -1599,7 +1609,7 @@ interface MutableArray extends EmberArray, MutableEnumerable { @return object @public */ - popObject(): T | undefined; + popObject(): T | null | undefined; /** Shift an object from start of array or nil if none are left. Works just like `shift()` but it is KVO-compliant. @@ -1632,7 +1642,7 @@ interface MutableArray extends EmberArray, MutableEnumerable { @return object same object passed as a param @public */ - unshiftObject(object: T): this; + unshiftObject(object: T): T; /** Adds the named objects to the beginning of the array. Defers notifying observers until all objects have been added. @@ -1892,6 +1902,167 @@ const MutableArray = Mixin.create(EmberArray, MutableEnumerable, { /** @module ember */ + +type AnyArray = EmberArray | Array; + +/** + * 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 = + | 'length' + | 'slice' + | 'indexOf' + | 'lastIndexOf' + | 'forEach' + | 'map' + | 'filter' + | 'find' + | 'every' + | 'reduce' + | 'includes'; + +/** + * These additional items must be redefined since `Omit` causes methods that return `this` to return the + * type at the time of the Omit. + */ +type RETURN_SELF_ARRAY_METHODS = + | '[]' + | 'clear' + | 'insertAt' + | 'removeAt' + | 'pushObjects' + | 'unshiftObjects' + | 'reverseObjects' + | 'setObjects' + | 'removeObject' + | 'removeObjects' + | 'addObject' + | 'addObjects' + | 'setEach'; + +// This is the same as MutableArray, but removes the actual native methods that exist on Array.prototype. +interface MutableArrayWithoutNative + extends Omit, IGNORED_MUTABLE_ARRAY_METHODS | RETURN_SELF_ARRAY_METHODS> { + /** + * Remove all elements from the array. This is useful if you + * want to reuse an existing array without having to recreate it. + */ + clear(): this; + /** + * This will use the primitive `replace()` method to insert an object at the + * specified index. + */ + insertAt(idx: number, object: T): this; + /** + * Remove an object at the specified index using the `replace()` primitive + * method. You can pass either a single index, or a start and a length. + */ + removeAt(start: number, len?: number): this; + /** + * Add the objects in the passed numerable to the end of the array. Defers + * notifying observers of the change until all objects are added. + */ + pushObjects(objects: AnyArray): this; + /** + * Adds the named objects to the beginning of the array. Defers notifying + * observers until all objects have been added. + */ + unshiftObjects(objects: AnyArray): this; + /** + * Reverse objects in the array. Works just like `reverse()` but it is + * KVO-compliant. + */ + reverseObjects(): this; + /** + * Replace all the receiver's content with content of the argument. + * If argument is an empty array receiver will be cleared. + */ + setObjects(objects: AnyArray): this; + /** + Remove all occurrences of an object in the array. + + ```javascript + let cities = ['Chicago', 'Berlin', 'Lima', 'Chicago']; + + cities.removeObject('Chicago'); // ['Berlin', 'Lima'] + cities.removeObject('Lima'); // ['Berlin'] + cities.removeObject('Tokyo') // ['Berlin'] + ``` + + @method removeObject + @param {*} obj object to remove + @return {EmberArray} receiver + @public + */ + removeObject(object: T): this; + /** + * Removes each object in the passed array from the receiver. + */ + removeObjects(objects: AnyArray): this; + /** + Push the object onto the end of the array if it is not already + present in the array. + + ```javascript + let cities = ['Chicago', 'Berlin']; + + cities.addObject('Lima'); // ['Chicago', 'Berlin', 'Lima'] + cities.addObject('Berlin'); // ['Chicago', 'Berlin', 'Lima'] + ``` + + @method addObject + @param {*} obj object to add, if not already present + @return {EmberArray} receiver + @public + */ + addObject(obj: T): this; + /** + * Adds each object in the passed enumerable to the receiver. + */ + addObjects(objects: AnyArray): this; + /** + Sets the value on the named property for each member. This is more + ergonomic than using other methods defined on this helper. If the object + implements Observable, the value will be changed to `set(),` otherwise + it will be set directly. `null` objects are skipped. + + ```javascript + let people = [{name: 'Joe'}, {name: 'Matt'}]; + + people.setEach('zipCode', '10011'); + // [{name: 'Joe', zipCode: '10011'}, {name: 'Matt', zipCode: '10011'}]; + ``` + + @method setEach + @param {String} key The key to set + @param {Object} value The object to set + @return {Object} receiver + @public + */ + setEach(key: K, value: T[K]): this; + /** + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property to a new + array, it will replace the current content. + + ```javascript + let peopleToMoon = ['Armstrong', 'Aldrin']; + + peopleToMoon.get('[]'); // ['Armstrong', 'Aldrin'] + + peopleToMoon.set('[]', ['Collins']); // ['Collins'] + peopleToMoon.get('[]'); // ['Collins'] + ``` + + @property [] + @return this + @public + */ + get '[]'(): this; + set '[]'(newValue: T[] | this); +} + /** The NativeArray mixin contains the properties needed to make the native Array support MutableArray and all of its dependent APIs. Unless you @@ -1904,10 +2075,7 @@ const MutableArray = Mixin.create(EmberArray, MutableEnumerable, { @uses Observable @public */ -interface NativeArray - extends Omit, 'every' | 'filter' | 'find' | 'forEach' | 'map' | 'reduce' | 'slice'>, - MutableArray, - Observable {} +interface NativeArray extends Array, Observable, MutableArrayWithoutNative {} let NativeArray = Mixin.create(MutableArray, Observable, { objectAt(idx: number) { diff --git a/packages/@ember/array/type-tests/index.ts b/packages/@ember/array/type-tests/index.ts index a15c8c150ef..e74f816bdaf 100644 --- a/packages/@ember/array/type-tests/index.ts +++ b/packages/@ember/array/type-tests/index.ts @@ -25,8 +25,11 @@ class Target { let target = new Target(); -expectTypeOf(arr).toMatchTypeOf>(); -expectTypeOf(arr).toMatchTypeOf>(); +// NativeArray does not exactly extend the interface of EmberArray and MutableArray, +// since native methods are not overwritten. +expectTypeOf(arr).not.toMatchTypeOf>(); +expectTypeOf(arr).not.toMatchTypeOf>(); + expectTypeOf(arr).toEqualTypeOf>(); expectTypeOf(arr.length).toEqualTypeOf(); @@ -37,9 +40,9 @@ expectTypeOf(arr.firstObject).toEqualTypeOf(); expectTypeOf(arr.lastObject).toEqualTypeOf(); -expectTypeOf(arr.slice()).toEqualTypeOf>(); -expectTypeOf(arr.slice(1)).toEqualTypeOf>(); -expectTypeOf(arr.slice(1, 2)).toEqualTypeOf>(); +expectTypeOf(arr.slice()).toEqualTypeOf(); +expectTypeOf(arr.slice(1)).toEqualTypeOf(); +expectTypeOf(arr.slice(1, 2)).toEqualTypeOf(); expectTypeOf(arr.indexOf(new Foo())).toEqualTypeOf(); // @ts-expect-error checks param type @@ -49,7 +52,7 @@ expectTypeOf(arr.lastIndexOf(new Foo())).toEqualTypeOf(); // @ts-expect-error checks param type arr.lastIndexOf('invalid'); -expectTypeOf(arr.forEach((item: Foo) => String(item))).toEqualTypeOf(arr); +expectTypeOf(arr.forEach((item: Foo) => String(item))).toEqualTypeOf(); arr.forEach((item, index, arr) => { expectTypeOf(this).toEqualTypeOf(); @@ -58,18 +61,19 @@ arr.forEach((item, index, arr) => { expectTypeOf(arr).toEqualTypeOf(arr); }); arr.forEach(function (_item) { - expectTypeOf(this).toEqualTypeOf(target); + // No-op }, target); expectTypeOf(arr.getEach('bar')).toEqualTypeOf>(); -expectTypeOf(arr.getEach('missing')).toEqualTypeOf>(); +// @ts-expect-error property is unknown +arr.getEach('missing'); expectTypeOf(arr.setEach('bar', 2)).toEqualTypeOf(arr); // @ts-expect-error string is not assignable to bar arr.setEach('bar', 'string'); -// Can set unknown property -expectTypeOf(arr.setEach('missing', 'anything')).toEqualTypeOf(arr); +// @ts-expect-error property is unknown +arr.setEach('missing', 'anything'); let mapped = arr.map((item, index, arr) => { expectTypeOf(item).toEqualTypeOf(); @@ -77,14 +81,15 @@ let mapped = arr.map((item, index, arr) => { expectTypeOf(arr).toEqualTypeOf(arr); return 1; }); -expectTypeOf(mapped).toEqualTypeOf>(); +expectTypeOf(mapped).toEqualTypeOf(); arr.map(function (_item) { - expectTypeOf(this).toEqualTypeOf(target); + return true; }, target); expectTypeOf(arr.mapBy('bar')).toEqualTypeOf>(); -expectTypeOf(arr.mapBy('missing')).toEqualTypeOf>(); +// @ts-expect-error property is unknown +arr.mapBy('missing'); let filtered = arr.filter((item, index, arr) => { expectTypeOf(item).toEqualTypeOf(); @@ -92,9 +97,8 @@ let filtered = arr.filter((item, index, arr) => { expectTypeOf(arr).toEqualTypeOf(arr); return true; }); -expectTypeOf(filtered).toEqualTypeOf>(); +expectTypeOf(filtered).toEqualTypeOf(); arr.filter(function (_item) { - expectTypeOf(this).toEqualTypeOf(target); return true; }, target); @@ -120,6 +124,7 @@ expectTypeOf(arr.findBy('bar')).toEqualTypeOf(); arr.findBy('bar', 1); // @ts-expect-error value has incorrect type arr.findBy('bar', 'invalid'); +// @ts-expect-error property is unknown arr.findBy('missing', 'whatever'); let isEvery = arr.every((item, index, arr) => { @@ -130,7 +135,6 @@ let isEvery = arr.every((item, index, arr) => { }); expectTypeOf(isEvery).toEqualTypeOf(); arr.every(function (_item) { - expectTypeOf(this).toEqualTypeOf(target); return true; }, target); @@ -138,6 +142,7 @@ expectTypeOf(arr.isEvery('bar')).toEqualTypeOf(); arr.isEvery('bar', 1); // @ts-expect-error value has incorrect type arr.isEvery('bar', 'invalid'); +// @ts-expect-error property is unknown arr.isEvery('missing', 'whatever'); let isAny = arr.any((item, index, arr) => { @@ -156,6 +161,7 @@ expectTypeOf(arr.isAny('bar')).toEqualTypeOf(); arr.isAny('bar', 1); // @ts-expect-error value has incorrect type arr.isAny('bar', 'invalid'); +// @ts-expect-error property is unknown arr.isAny('missing', 'whatever'); let reduced = arr.reduce((summation, item, index, arr) => { @@ -166,16 +172,14 @@ let reduced = arr.reduce((summation, item, index, arr) => { return 1; }, 1); expectTypeOf(reduced).toEqualTypeOf(); -// NOTE: This doesn't match native behavior and is a bit weird -expectTypeOf(arr.reduce((summation, _item) => summation)).toEqualTypeOf(); +expectTypeOf(arr.reduce((summation, _item) => summation)).toEqualTypeOf(); expectTypeOf(arr.invoke('hi')).toEqualTypeOf>(); expectTypeOf(arr.invoke('withArgs', 1, 'two')).toEqualTypeOf>(); // @ts-expect-error Doesn't allow calling with invalid args arr.invoke('withArgs', 'invalid'); -expectTypeOf(arr.invoke('missing')).toEqualTypeOf>(); -// Currently, args passed to unrecognized methods are ignored -arr.invoke('missing', 'invalid'); +// @ts-expect-error Doesn't allow calling with invalid method +arr.invoke('missing'); expectTypeOf(arr.toArray()).toEqualTypeOf(); diff --git a/packages/@ember/array/type-tests/mutable.ts b/packages/@ember/array/type-tests/mutable.ts index 4672c0be375..db1a55e73a3 100644 --- a/packages/@ember/array/type-tests/mutable.ts +++ b/packages/@ember/array/type-tests/mutable.ts @@ -1,63 +1,67 @@ -import type MutableArray from '@ember/array/mutable'; -import { A } from '@ember/array'; +import MutableArray from '@ember/array/mutable'; import { expectTypeOf } from 'expect-type'; -class Foo {} +class Foo { + constructor(public name: string) {} +} -let foo = new Foo(); +let foo = new Foo('test'); -let arr = A([foo]); +let originalArr = [foo]; +// This is not really the ideal way to set things up. +MutableArray.apply(originalArr); +let arr = originalArr as unknown as MutableArray; expectTypeOf(arr).toMatchTypeOf>(); expectTypeOf(arr.replace(1, 1, [foo])).toEqualTypeOf(); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.replace(1, 1, ['invalid']); expectTypeOf(arr.clear()).toEqualTypeOf(arr); expectTypeOf(arr.insertAt(1, foo)).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.insertAt(1, 'invalid'); expectTypeOf(arr.removeAt(1, 1)).toEqualTypeOf(arr); -expectTypeOf(arr.pushObject(foo)).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +expectTypeOf(arr.pushObject(foo)).toEqualTypeOf(foo); +// @ts-expect-error invalid item arr.pushObject('invalid'); expectTypeOf(arr.pushObjects([foo])).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.pushObjects(['invalid']); -expectTypeOf(arr.popObject()).toEqualTypeOf(); +expectTypeOf(arr.popObject()).toEqualTypeOf(); expectTypeOf(arr.shiftObject()).toEqualTypeOf(); -expectTypeOf(arr.unshiftObject(foo)).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +expectTypeOf(arr.unshiftObject(foo)).toEqualTypeOf(foo); +// @ts-expect-error invalid item arr.unshiftObject('invalid'); expectTypeOf(arr.unshiftObjects([foo])).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.unshiftObjects(['invalid']); expectTypeOf(arr.reverseObjects()).toEqualTypeOf(arr); expectTypeOf(arr.setObjects([foo])).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.setObjects(['invalid']); expectTypeOf(arr.removeObject(foo)).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.removeObject('invalid'); expectTypeOf(arr.addObject(foo)).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.addObject('invalid'); expectTypeOf(arr.addObjects([foo])).toEqualTypeOf(arr); -// TODO: Why doesn't this fail? +// @ts-expect-error invalid item arr.addObjects(['invalid']); diff --git a/packages/@ember/debug/data-adapter.ts b/packages/@ember/debug/data-adapter.ts index a7cf8ace322..5b3cbcce6c3 100644 --- a/packages/@ember/debug/data-adapter.ts +++ b/packages/@ember/debug/data-adapter.ts @@ -544,7 +544,7 @@ export default class DataAdapter extends EmberObject { @method getModelTypes @return {Array} Array of model types. */ - getModelTypes(): NativeArray<{ klass: unknown; name: string }> { + getModelTypes(): Array<{ klass: unknown; name: string }> { let containerDebugAdapter = this.containerDebugAdapter; let stringTypes = containerDebugAdapter.canCatalogEntriesByType('model') diff --git a/type-tests/preview/@ember/array-test/array.ts b/type-tests/preview/@ember/array-test/array.ts index 8f7df294f56..77111c7f17e 100755 --- a/type-tests/preview/@ember/array-test/array.ts +++ b/type-tests/preview/@ember/array-test/array.ts @@ -3,6 +3,7 @@ import type Array from '@ember/array'; import { A } from '@ember/array'; import type MutableArray from '@ember/array/mutable'; import { expectTypeOf } from 'expect-type'; +import NativeArray from '@ember/array/-private/native-array'; class Person extends EmberObject { name = ''; @@ -26,13 +27,11 @@ expectTypeOf(people.objectAt(0)).toEqualTypeOf(); expectTypeOf(people.objectsAt([1, 2, 3])).toEqualTypeOf>(); expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf(); -expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf>(); expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf(); -expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf(); expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf(); -expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf< - MutableArray ->(); expectTypeOf(people.get('[]')).toEqualTypeOf(); expectTypeOf(people.get('[]').get('firstObject')).toEqualTypeOf(); diff --git a/type-tests/preview/ember/array.ts b/type-tests/preview/ember/array.ts index b5db931246a..17d0c82065f 100755 --- a/type-tests/preview/ember/array.ts +++ b/type-tests/preview/ember/array.ts @@ -23,13 +23,11 @@ expectTypeOf(people.objectAt(0)).toEqualTypeOf(); expectTypeOf(people.objectsAt([1, 2, 3])).toEqualTypeOf>(); expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf(); -expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf>(); expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf(); -expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf(); expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf(); -expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf< - Ember.MutableArray ->(); expectTypeOf(people.get('[]')).toEqualTypeOf(); expectTypeOf(people.get('[]').get('firstObject')).toEqualTypeOf(); @@ -48,15 +46,14 @@ if (first) { expectTypeOf(first.get('isHappy')).toBeBoolean(); } -const letters: Ember.Array = Ember.A(['a', 'b', 'c']); +const letters: Ember.NativeArray = Ember.A(['a', 'b', 'c']); const codes = letters.map((item, index, array) => { expectTypeOf(item).toBeString(); expectTypeOf(index).toBeNumber(); - expectTypeOf(array).toEqualTypeOf>(); + expectTypeOf(array).toEqualTypeOf(); return item.charCodeAt(0); }); -expectTypeOf(codes).toMatchTypeOf(); -expectTypeOf(codes).toEqualTypeOf>(); +expectTypeOf(codes).toEqualTypeOf(); const value = '1,2,3'; const filters = Ember.A(value.split(',')); diff --git a/type-tests/preview/ember/ember-module-tests.ts b/type-tests/preview/ember/ember-module-tests.ts index fd01363e59a..b6b355d067e 100644 --- a/type-tests/preview/ember/ember-module-tests.ts +++ b/type-tests/preview/ember/ember-module-tests.ts @@ -153,7 +153,7 @@ expectTypeOf(Ember.Application.create()).toEqualTypeOf(); expectTypeOf(new Ember.ApplicationInstance()).toEqualTypeOf(); expectTypeOf(Ember.ApplicationInstance.create()).toEqualTypeOf(); // Ember.Array -const a1: Ember.Array = Ember.A([]); +const a1: Ember.NativeArray = Ember.A([]); // @ts-expect-error const a2: Ember.Array = {}; // Ember.ArrayProxy @@ -232,13 +232,13 @@ class UsesMixin extends Ember.Object { } } // Ember.MutableArray -const ma1: Ember.MutableArray = Ember.A(['money', 'in', 'the', 'bananna', 'stand']); -expectTypeOf(ma1.addObject('!')).toBeString(); +const ma1: Ember.NativeArray = Ember.A(['money', 'in', 'the', 'bananna', 'stand']); +expectTypeOf(ma1.addObject('!')).toMatchTypeOf(ma1); // @ts-expect-error ma1.filterBy(''); expectTypeOf(ma1.firstObject).toEqualTypeOf(); expectTypeOf(ma1.lastObject).toEqualTypeOf(); -const ma2: Ember.MutableArray<{ name: string }> = Ember.A([ +const ma2: Ember.NativeArray<{ name: string }> = Ember.A([ { name: 'chris' }, { name: 'dan' }, { name: 'james' }, diff --git a/type-tests/preview/ember/ember-tests.ts b/type-tests/preview/ember/ember-tests.ts index 18bfa6cccc7..bd2921e9503 100755 --- a/type-tests/preview/ember/ember-tests.ts +++ b/type-tests/preview/ember/ember-tests.ts @@ -112,14 +112,13 @@ people.invoke('sayHello'); // @ts-expect-error people.invoke('name'); -type Arr = Ember.NativeArray< - Ember.Object & { - name?: string; - } ->; -const arr: Arr = Ember.A([Ember.Object.create(), Ember.Object.create()]); -expectTypeOf(arr.setEach('name', 'unknown')).toEqualTypeOf(); -expectTypeOf(arr.setEach('name', undefined)).toEqualTypeOf(); +class Obj extends Ember.Object { + name?: string; +} + +const arr: Ember.NativeArray = Ember.A([Ember.Object.create(), Ember.Object.create()]); +expectTypeOf(arr.setEach('name', 'unknown')).toEqualTypeOf(arr); +expectTypeOf(arr.setEach('name', undefined)).toEqualTypeOf(arr); expectTypeOf(arr.getEach('name')).toEqualTypeOf>(); // @ts-expect-error arr.setEach('age', 123); diff --git a/types/preview/@ember/array/-private/native-array.d.ts b/types/preview/@ember/array/-private/native-array.d.ts index 814eb825abd..50781a30d77 100644 --- a/types/preview/@ember/array/-private/native-array.d.ts +++ b/types/preview/@ember/array/-private/native-array.d.ts @@ -1,11 +1,172 @@ declare module '@ember/array/-private/native-array' { import Observable from '@ember/object/observable'; + import EmberArray from '@ember/array'; import MutableArray from '@ember/array/mutable'; import Mixin from '@ember/object/mixin'; // Get an alias to the global Array type to use in inner scope below. type Array = T[]; + type AnyArray = EmberArray | Array; + + /** + * 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 = + | 'length' + | 'slice' + | 'indexOf' + | 'lastIndexOf' + | 'forEach' + | 'map' + | 'filter' + | 'find' + | 'every' + | 'reduce' + | 'includes'; + + /** + * These additional items must be redefined since `Omit` causes methods that return `this` to return the + * type at the time of the Omit. + */ + type RETURN_SELF_ARRAY_METHODS = + | '[]' + | 'clear' + | 'insertAt' + | 'removeAt' + | 'pushObjects' + | 'unshiftObjects' + | 'reverseObjects' + | 'setObjects' + | 'removeObject' + | 'removeObjects' + | 'addObject' + | 'addObjects' + | 'setEach'; + + // This is the same as MutableArray, but removes the actual native methods that exist on Array.prototype. + interface MutableArrayWithoutNative + extends Omit, IGNORED_MUTABLE_ARRAY_METHODS | RETURN_SELF_ARRAY_METHODS> { + /** + * Remove all elements from the array. This is useful if you + * want to reuse an existing array without having to recreate it. + */ + clear(): this; + /** + * This will use the primitive `replace()` method to insert an object at the + * specified index. + */ + insertAt(idx: number, object: T): this; + /** + * Remove an object at the specified index using the `replace()` primitive + * method. You can pass either a single index, or a start and a length. + */ + removeAt(start: number, len?: number): this; + /** + * Add the objects in the passed numerable to the end of the array. Defers + * notifying observers of the change until all objects are added. + */ + pushObjects(objects: AnyArray): this; + /** + * Adds the named objects to the beginning of the array. Defers notifying + * observers until all objects have been added. + */ + unshiftObjects(objects: AnyArray): this; + /** + * Reverse objects in the array. Works just like `reverse()` but it is + * KVO-compliant. + */ + reverseObjects(): this; + /** + * Replace all the receiver's content with content of the argument. + * If argument is an empty array receiver will be cleared. + */ + setObjects(objects: AnyArray): this; + /** + Remove all occurrences of an object in the array. + + ```javascript + let cities = ['Chicago', 'Berlin', 'Lima', 'Chicago']; + + cities.removeObject('Chicago'); // ['Berlin', 'Lima'] + cities.removeObject('Lima'); // ['Berlin'] + cities.removeObject('Tokyo') // ['Berlin'] + ``` + + @method removeObject + @param {*} obj object to remove + @return {EmberArray} receiver + @public + */ + removeObject(object: T): this; + /** + * Removes each object in the passed array from the receiver. + */ + removeObjects(objects: AnyArray): this; + /** + Push the object onto the end of the array if it is not already + present in the array. + + ```javascript + let cities = ['Chicago', 'Berlin']; + + cities.addObject('Lima'); // ['Chicago', 'Berlin', 'Lima'] + cities.addObject('Berlin'); // ['Chicago', 'Berlin', 'Lima'] + ``` + + @method addObject + @param {*} obj object to add, if not already present + @return {EmberArray} receiver + @public + */ + addObject(obj: T): this; + /** + * Adds each object in the passed enumerable to the receiver. + */ + addObjects(objects: AnyArray): this; + /** + Sets the value on the named property for each member. This is more + ergonomic than using other methods defined on this helper. If the object + implements Observable, the value will be changed to `set(),` otherwise + it will be set directly. `null` objects are skipped. + + ```javascript + let people = [{name: 'Joe'}, {name: 'Matt'}]; + + people.setEach('zipCode', '10011'); + // [{name: 'Joe', zipCode: '10011'}, {name: 'Matt', zipCode: '10011'}]; + ``` + + @method setEach + @param {String} key The key to set + @param {Object} value The object to set + @return {Object} receiver + @public + */ + setEach(key: K, value: T[K]): this; + /** + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property to a new + array, it will replace the current content. + + ```javascript + let peopleToMoon = ['Armstrong', 'Aldrin']; + + peopleToMoon.get('[]'); // ['Armstrong', 'Aldrin'] + + peopleToMoon.set('[]', ['Collins']); // ['Collins'] + peopleToMoon.get('[]'); // ['Collins'] + ``` + + @property [] + @return this + @public + */ + get '[]'(): this; + set '[]'(newValue: T[] | this); + } + /** * The NativeArray mixin contains the properties needed to make the native * Array support Ember.MutableArray and all of its dependent APIs. Unless you @@ -13,10 +174,7 @@ declare module '@ember/array/-private/native-array' { * false, this will be applied automatically. Otherwise you can apply the mixin * at anytime by calling `Ember.NativeArray.apply(Array.prototype)`. */ - interface NativeArray - extends Omit, 'every' | 'filter' | 'find' | 'forEach' | 'map' | 'reduce' | 'slice'>, - Observable, - MutableArray {} + interface NativeArray extends Array, Observable, MutableArrayWithoutNative {} const NativeArray: Mixin; export default NativeArray; diff --git a/types/preview/@ember/array/index.d.ts b/types/preview/@ember/array/index.d.ts index cd149da2d1b..efb6468b9cc 100644 --- a/types/preview/@ember/array/index.d.ts +++ b/types/preview/@ember/array/index.d.ts @@ -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 = `${keyof T & string}.${string}`; + /** * This module implements Observer-friendly Array-like behavior. This mixin is picked up by the * Array class as well as other controllers, etc. that want to appear to be arrays. @@ -67,13 +70,14 @@ declare module '@ember/array' { * Alias for `mapBy` */ getEach(key: K): NativeArray; + getEach>(key: K): NativeArray; /** * Sets the value on the named property for each member. This is more * ergonomic than using other methods defined on this helper. If the object * implements Ember.Observable, the value will be changed to `set(),` otherwise * it will be set directly. `null` objects are skipped. */ - setEach(key: K, value: T[K]): void; + setEach(key: K, value: T[K]): this; /** * Maps all of the items in the enumeration to another value, returning * a new array. This method corresponds to `map()` defined in JavaScript 1.6. @@ -87,7 +91,7 @@ declare module '@ember/array' { * property on all items in the enumeration. */ mapBy(key: K): NativeArray; - mapBy(key: string): NativeArray; + mapBy>(key: K): NativeArray; /** * Returns an array with all of the items in the enumeration that the passed * function returns true for. This method corresponds to `filter()` defined in @@ -111,12 +115,14 @@ declare module '@ember/array' { * this will match any property that evaluates to `true`. */ filterBy(key: K, value?: T[K]): NativeArray; + filterBy>(key: K, value?: unknown): NativeArray; /** * Returns an array with the items that do not have truthy values for * key. You can pass an optional second argument with the target value. Otherwise * this will match any property that evaluates to false. */ rejectBy(key: K, value?: T[K]): NativeArray; + rejectBy>(key: K, value?: unknown): NativeArray; /** * Returns the first item in the array for which the callback returns true. * This method works similar to the `filter()` method defined in JavaScript 1.6 @@ -136,6 +142,7 @@ declare module '@ember/array' { * this will match any property that evaluates to `true`. */ findBy(key: K, value?: T[K]): T | undefined; + findBy>(key: K, value?: unknown): T | undefined; /** * Returns `true` if the passed function returns true for every item in the * enumeration. This corresponds with the `every()` method in JavaScript 1.6. @@ -150,6 +157,7 @@ declare module '@ember/array' { * than using a callback. */ isEvery(key: K, value?: T[K]): boolean; + isEvery>(key: K, value?: unknown): boolean; /** * Returns `true` if the passed function returns true for any item in the * enumeration. @@ -164,12 +172,16 @@ declare module '@ember/array' { * than using a callback. */ isAny(key: K, value?: T[K]): boolean; + isAny>(key: K, value?: unknown): boolean; /** * This will combine the values of the enumerator into a single value. It * is a useful way to collect a summary value from an enumeration. This * corresponds to the `reduce()` method defined in JavaScript 1.8. */ - reduce: T[]['reduce']; + reduce( + callback: (summation: V, current: T, index: number, arr: this) => V, + initialValue?: V + ): V; /** * Invokes the named method on every object in the receiver that * implements it. This method corresponds to the implementation in @@ -178,7 +190,7 @@ declare module '@ember/array' { invoke>( methodName: M, ...args: MethodParams - ): Array>; + ): NativeArray>; /** * Simply converts the enumerable into a genuine array. The order is not * guaranteed. Corresponds to the method implemented by Prototype. @@ -196,7 +208,7 @@ declare module '@ember/array' { * Converts the enumerable into an array and sorts by the keys * specified in the argument. */ - sortBy(...properties: string[]): NativeArray; + sortBy(...properties: string[]): T[]; /** * Returns a new enumerable that excludes the passed value. The default * implementation returns an array regardless of the receiver type. @@ -219,8 +231,8 @@ declare module '@ember/array' { * this property, it will return this. If you set this property to a new * array, it will replace the current content. */ - get '[]'(): NativeArray; - set '[]'(newValue: NativeArray); + get '[]'(): this; + set '[]'(newValue: T[] | Array); } // Ember.Array rather than Array because the `array-type` lint rule doesn't realize the global is shadowed const Array: Mixin; diff --git a/types/preview/@ember/array/mutable.d.ts b/types/preview/@ember/array/mutable.d.ts index 30c365bb953..3890fe05da6 100644 --- a/types/preview/@ember/array/mutable.d.ts +++ b/types/preview/@ember/array/mutable.d.ts @@ -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; /** * Remove all elements from the array. This is useful if you * want to reuse an existing array without having to recreate it. @@ -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; /** * Shift an object from start of array or nil if none are left. Works just * like `shift()` but it is KVO-compliant. */ - shiftObject(): T; + shiftObject(): T | null | undefined; /** * Unshift an object to start of array. Works just like `unshift()` but it is * KVO-compliant. @@ -82,7 +82,7 @@ declare module '@ember/array/mutable' { /** * __Required.__ You must implement this method to apply this mixin. */ - addObject(object: T): T; + addObject(object: T): this; /** * Adds each object in the passed enumerable to the receiver. */