Skip to content

Commit

Permalink
Improve types for Ember Arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
wagenet committed Nov 7, 2022
1 parent 0580eed commit cfaf5d5
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 110 deletions.
5 changes: 3 additions & 2 deletions packages/@ember/-internals/glimmer/lib/utils/iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IteratorDelegate> {
if (iterable instanceof EachInWrapper) {
Expand Down Expand Up @@ -100,11 +101,11 @@ class ArrayIterator extends BoundedIterator {
}

class EmberArrayIterator extends BoundedIterator {
static from(iterable: EmberArray<unknown>) {
static from(iterable: EmberArray<unknown> | NativeArray<unknown>) {
return iterable.length > 0 ? new this(iterable) : null;
}

constructor(private array: EmberArray<unknown>) {
constructor(private array: EmberArray<unknown> | NativeArray<unknown>) {
super(array.length);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/@ember/-internals/utils/lib/ember-array.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -7,6 +7,6 @@ export function setEmberArray(obj: object) {
EMBER_ARRAYS.add(obj);
}

export function isEmberArray(obj: unknown): obj is EmberArray<unknown> {
export function isEmberArray(obj: unknown): obj is EmberArrayLike<unknown> {
return EMBER_ARRAYS.has(obj as object);
}
8 changes: 8 additions & 0 deletions packages/@ember/-internals/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export type AnyFn = (...args: any[]) => any;

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

export type MethodNamesOf<T> = {
[K in keyof T]: T[K] extends AnyFn ? K : never;
}[keyof T];

export type MethodParams<T, M extends MethodNamesOf<T>> = Parameters<MethodsOf<T>[M]>;

export type MethodReturns<T, M extends MethodNamesOf<T>> = ReturnType<MethodsOf<T>[M]>;

export type OmitFirst<F> = F extends [any, ...infer R] ? R : [];
218 changes: 193 additions & 25 deletions packages/@ember/array/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, K extends string> = K extends keyof T ? T[K] : unknown;
export type EmberArrayLike<T> = EmberArray<T> | NativeArray<T>;

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

const EMPTY_ARRAY = Object.freeze([] as const);

Expand Down Expand Up @@ -319,7 +322,8 @@ interface EmberArray<T> extends Enumerable {
@return this
@public
*/
'[]': this;
get '[]'(): this;
set '[]'(newValue: T[] | EmberArray<T>);
/**
The first object in the array, or `undefined` if the array is empty.
Expand Down Expand Up @@ -515,7 +519,8 @@ interface EmberArray<T> extends Enumerable {
@return {Array} The mapped array.
@public
*/
getEach<K extends string>(key: K): NativeArray<Value<T, K>>;
getEach<K extends keyof T>(key: K): NativeArray<T[K]>;
getEach<K extends CompoundPropertyKey<T>>(key: K): NativeArray<unknown>;
/**
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
Expand All @@ -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;
/**
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.
Expand Down Expand Up @@ -593,7 +598,8 @@ interface EmberArray<T> extends Enumerable {
@return {Array} The mapped array.
@public
*/
mapBy<K extends string>(key: K): NativeArray<Value<T, K>>;
mapBy<K extends keyof T>(key: K): NativeArray<T[K]>;
mapBy<K extends CompoundPropertyKey<T>>(key: K): NativeArray<unknown>;
/**
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).
Expand Down Expand Up @@ -746,10 +752,6 @@ interface EmberArray<T> extends Enumerable {
@public
*/
rejectBy(key: string, value?: unknown): NativeArray<T>;
find<S extends T, Target = void>(
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.
Expand Down Expand Up @@ -792,6 +794,10 @@ interface EmberArray<T> extends Enumerable {
@return {Object} Found item or `undefined`.
@public
*/
find<S extends T, Target = void>(
predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
thisArg?: Target
): S | undefined;
find<Target = void>(
callback: (this: Target, item: T, index: number, arr: this) => unknown,
target?: Target
Expand Down Expand Up @@ -824,7 +830,8 @@ interface EmberArray<T> extends Enumerable {
@return {Object} found item or `undefined`
@public
*/
findBy<K extends string>(key: K, value?: Value<T, K>): T | undefined;
findBy<K extends keyof T>(key: K, value?: T[K]): T | undefined;
findBy<K extends CompoundPropertyKey<T>>(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.
Expand Down Expand Up @@ -904,7 +911,8 @@ interface EmberArray<T> extends Enumerable {
@since 1.3.0
@public
*/
isEvery<K extends string>(key: K, value?: Value<T, K>): boolean;
isEvery<K extends keyof T>(key: K, value?: T[K]): boolean;
isEvery<K extends CompoundPropertyKey<T>>(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
Expand Down Expand Up @@ -972,7 +980,8 @@ interface EmberArray<T> extends Enumerable {
@since 1.3.0
@public
*/
isAny<K extends string>(key: K, value?: Value<T, K>): boolean;
isAny<K extends keyof T>(key: K, value?: T[K]): boolean;
isAny<K extends CompoundPropertyKey<T>>(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
Expand Down Expand Up @@ -1061,10 +1070,10 @@ interface EmberArray<T> extends Enumerable {
@return {Array} return values from calling invoke.
@public
*/
invoke<K extends string>(
methodName: K,
...args: Value<T, K> extends AnyFn ? Parameters<Value<T, K>> : unknown[]
): NativeArray<Value<T, K> extends AnyFn ? ReturnType<Value<T, K>> : unknown>;
invoke<M extends MethodNamesOf<T>>(
methodName: M,
...args: MethodParams<T, M>
): NativeArray<MethodReturns<T, M>>;
/**
Simply converts the object into a genuine array. The order is not
guaranteed. Corresponds to the method implemented by Prototype.
Expand All @@ -1086,7 +1095,7 @@ interface EmberArray<T> extends Enumerable {
@return {Array} the array without null and undefined elements.
@public
*/
compact(): NativeArray<Exclude<T, null>>;
compact(): NativeArray<NonNullable<T>>;
/**
Used to determine if the array contains the passed object.
Returns `true` if found, `false` otherwise.
Expand Down Expand Up @@ -1176,6 +1185,7 @@ interface EmberArray<T> extends Enumerable {
@public
*/
uniqBy(key: string): NativeArray<T>;
uniqBy(callback: (value: T) => unknown): NativeArray<T>;
/**
Returns a new array that excludes the passed value. The default
implementation returns an array regardless of the receiver type.
Expand Down Expand Up @@ -1567,7 +1577,7 @@ interface MutableArray<T> extends EmberArray<T>, 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.
Expand Down Expand Up @@ -1599,7 +1609,7 @@ interface MutableArray<T> extends EmberArray<T>, 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.
Expand Down Expand Up @@ -1632,7 +1642,7 @@ interface MutableArray<T> extends EmberArray<T>, 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.
Expand Down Expand Up @@ -1892,6 +1902,167 @@ const MutableArray = Mixin.create(EmberArray, MutableEnumerable, {
/**
@module ember
*/

type AnyArray<T> = EmberArray<T> | Array<T>;

/**
* 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<T>
extends Omit<MutableArray<T>, 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<T>): this;
/**
* Adds the named objects to the beginning of the array. Defers notifying
* observers until all objects have been added.
*/
unshiftObjects(objects: AnyArray<T>): 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<T>): 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<T>): 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<T>): 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<K extends keyof T>(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
Expand All @@ -1904,10 +2075,7 @@ const MutableArray = Mixin.create(EmberArray, MutableEnumerable, {
@uses Observable
@public
*/
interface NativeArray<T>
extends Omit<Array<T>, 'every' | 'filter' | 'find' | 'forEach' | 'map' | 'reduce' | 'slice'>,
MutableArray<T>,
Observable {}
interface NativeArray<T> extends Array<T>, Observable, MutableArrayWithoutNative<T> {}

let NativeArray = Mixin.create(MutableArray, Observable, {
objectAt(idx: number) {
Expand Down
Loading

0 comments on commit cfaf5d5

Please sign in to comment.