types icon indicating copy to clipboard operation
types copied to clipboard

addIndex

Open Harris-Miller opened this issue 2 years ago • 1 comments

Draft branch: https://github.com/ramda/types/tree/addIndex

Current issues

addIndex is particularly unique in terms of type definitions because of how the return type depends on the given function

Let's use filter as an example

// Special case for filter
export function addIndex<T>(
  fn: (f: (item: T) => boolean, list: readonly T[]) => T[],
): _.F.Curry<(a: (item: T, idx: number, list: T[]) => boolean, b: readonly T[]) => T[]>;

To actually use it...

const filterIndexed = addIndex<number>(filter);

filterIndexed(predicate, [1, 2, 3, 4]); // ok
filterIndexed(predicate, ['a', 'b', 'c', 'd']); // error

The problem here is that you have to declare the generic for the type of the Array you intend to use it for. This defeats the purpose of generics in general, however, not passing the generic means that the type is unknown, which means the return type for all use cases is unknown[]

const filterIndexed = addIndex(filter);

filterIndexed(predicate, [1, 2, 3, 4]); // returns `unknown[]`
filterIndexed(predicate, ['a', 'b', 'c', 'd']); // returns `unknown[]`

That at least lets you use filterIndexed for any type, but then you lose all type inference. Not just for the the list, but for the predicate as well

addIndex<string>(filter)(isEven, ['a', 'b', 'c', 'd']); // error on isEven, because `(a: number) => boolean !== (a: string) => boolean`
addIndex(filter)(isEven, ['a', 'b', 'c', 'd']); // no error, because everything here is `unknown`

isEven also needs to be isEven = (item: number, i: number, list: number[]) => boolean otherwise it errors as well. This means that the inner fn type needs to support all overloads

Fix proposals

TODO

Harris-Miller avatar Mar 30 '23 19:03 Harris-Miller

Played around with this a bit and ended up with this: https://tsplay.dev/mqq5jm

Its not perfect (eg; auto type inference confuses map for forEach), however it maintains the created function's generic signature, meaning you don't have to create a new instance of addIndex(map) for every different kind of array type you use it on.

Also its entirely possible this is already well known, but while working on this I thought of an approach to defining overloads that return generic functions with multiple curried call signatures:

declare function indexedReduceSignature<T, R>(
  callback: IndexedReduceCallback<T, R>,
  acc: R,
  list: T[],
): R;
declare function indexedReduceSignature<T, R>(
  callback: IndexedReduceCallback<T, R>,
  acc: R,
): (list: T[]) => R;

declare function indexedMapSignature<T, N>(
  callback: IndexedMapCallback<T, N>,
  list: T[],
): N[];
declare function indexedMapSignature<T, N>(
  callback: IndexedMapCallback<T, N>,
): (list: T[]) => N[];

/**
 * Reduce
 */
export function myAddIndex(
  fn: (
    callback: NonIndexedReduceCallback<any, any>,
    acc: any,
    list: any[],
  ) => any,
): typeof indexedReduceSignature;
/**
 * Map
 */
export function myAddIndex(
  fn: (callback: NonIndexedMapCallback<any, any>, list: any[]) => any[],
): typeof indexedMapSignature;

By defining the different possible output shapes as their own functions with their own overloads, and then referencing them with typeof , typescript will maintain the integrity of each signature. Perhaps this could be used for addressing some of the other problematic type signatures.

Edit: The approach I described is not actually as powerful as I though, I tried applying it to the take function types but it ends up with the same issues the current one already has: https://tsplay.dev/w2QaYW. You can see the f variable there has a return type of unknown[].

TClark1011 avatar Oct 16 '23 05:10 TClark1011