11
votes

Say I have the following foldable interface:

export interface Foldable<F> {
  reduce: <A>(fn: (b: A, a: A) => A, initial: A, foldable: F) => A;
}

And then I want to implement it for array:

export const getArrayFold = <A>(): Foldable<Array<A>> => {
  return {
    reduce: (fn, initial, array) => {
      return array.reduce(fn, initial);
    }
  };
};

But the compiler complains with:

Argument of type '(b: A, a: A) => A' is not assignable to parameter of type '(previousValue: A, currentValue: A, currentIndex: number, array: A[]) => A'. Types of parameters 'a' and 'currentValue' are incompatible. Type 'A' is not assignable to type 'A'. Two different types with this name exist, but they are unrelated.

I don't understand how there are two different types of A here.

2

2 Answers

6
votes

There are two errors:

  • You need to provide which type is array. You can't get it from single generic Array<T>, you need to introduce both T and Array<T>.
  • Your type for function that is consumed by reduce is not adequate. Correct one: (previousValue: A, currentValue: F) => A

Explanation:

If you provide initial value with type (e.g. string) to reduce function, previousValue parameter is always same as inital.

See official TypeScript reduce declaration:

interface Array<T> {
    reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: ReadonlyArray<T>) => U, initialValue: U): U;
}

Full code (refactored)

interface Foldable<F, T> {
    reduce: <A>(
        fn: (previousValue: A, currentValue: T) => A,
        initial: A,
        foldable: F
    ) => A;
}

const getArrayFold = <T>(): Foldable<T[], T> => ({
    reduce(fn, initial, array) {
        return array.reduce(fn, initial);
    }
});

// Real implementation usage
const array: number[] = [1, 2, 3]
const initial: string = "";
const fn: (previousValue: string, currentValue: number) => string = (a, b) => a + b;

const newValue: string = getArrayFold().reduce(fn, initial, array);

See code on TypeScript playground

2
votes

It's easier to see what is going on if you change the generic type names:

export const getArrayFold = <R>(): Foldable<Array<R>> => {

Now you will get Type 'R' is not assignable to type 'A'. array.reduce uses different types for the current value and previous value, so you have type A (the generic type from your interface) and type R from your getArrayFold function.

You have not actually passed in the generic type A to reduce, so it considers it to be A from the interface which essentially just means it can't determine what the type is supposed to be.

One way I have found to do this is to allow your interface to specify the type of both A and F:

export interface Foldable<F, A> {
  reduce: (fn: (b: A, a: A) => A, initial: A, foldable: F) => A;

Now you can write your array function as

getArrayFold = <R>(): Foldable<Array<R>, R>

When you call it, you can do

getArrayFold<string>().reduce((a, b) => a + b, '', ['hello', 'world']);

This will give you type safety so you can't use 0 as a value or .toFixed on the a/b properties or things like that.