Description
EDIT: updated the types in example to match what works perfectly in Flow type
TypeScript Version: 2.2.1 / nightly (2.2.0-dev.201xxxxx)
TypeScript is unable to infer types through higher-order functions.
Code
Given some imaginary class SetOf
, which is just some set of values we want to compose operations over... We'll try the following:
// This is a contrived class. We could do the same thing with Observables, etc.
class SetOf<A> {
_store: A[];
add(a: A) {
this._store.push(a);
}
transform<B>(transformer: (a: SetOf<A>) => SetOf<B>): SetOf<B> {
return transformer(this);
}
forEach(fn: (a: A, index: number) => void) {
this._store.forEach((a, i) => fn(a, i));
}
}
function compose<A, B, C, D, E>(
fnA: (a: SetOf<A>) => SetOf<B>,
fnB: (b: SetOf<B>) => SetOf<C>,
fnC: (c: SetOf<C>) => SetOf<D>,
fnD: (c: SetOf<D>) => SetOf<E>,
):(x: SetOf<A>) => SetOf<E>;
/* ... etc ... */
function compose<T>(...fns: ((x: T) => T)[]): (x: T) => T {
return (x: T) => fns.reduce((prev, fn) => fn(prev), x);
}
function map<A, B>(fn: (a: A) => B): (s: SetOf<A>) => SetOf<B> {
return (a: SetOf<A>) => {
const b: SetOf<B> = new SetOf();
a.forEach(x => b.add(fn(x)));
return b;
}
}
function filter<A>(predicate: (a: A) => boolean): (s: SetOf<A>) => SetOf<A> {
return (a: SetOf<A>) => {
const result = new SetOf<A>();
a.forEach(x => {
if (predicate(x)) result.add(x);
});
return result;
}
}
const testSet = new SetOf();
testSet.add(1);
testSet.add(2);
testSet.add(3);
// THE PROBLEM IS HERE
// all functions below are unable to infer any types.
// The user will be required to annotate the types manually at each step.
testSet.transform(
compose(
filter(x => x % 1 === 0),
map(x => x + x),
map(x => x + '!!!'),
map(x => x.toUpperCase())
)
)
testSet.transform(
compose(
filter(x => x % 1 === 0),
map(x => x + x),
map(x => 123), // Whoops a bug
map(x => x.toUpperCase()) // causes an error!
)
)
Libraries Where This Problem Will Exist
RxJS
We want to move RxJS over to using "lettable operators". That means operators that are built in a similar fashion to what you see in the example able. The current prototype augmentation is untenable for large applications, and makes tree-shaking hard if not impossible with regards to Rx.
Ramda
A widely popular functional programming library will undoubtedly have the same issues. Especially give that (I think) the compose
function shown above exists (probably in a better form) within Ramdba.
Redux
combineReducers
in redux is liable to have this same problem, at least when dealing with typed reducers. I don't think the reducer's types can be inferred inside of that call. However, I'm not sure it's a common use case for Redux to have inline reducers in a combineReducers
call.
Plain JavaScript
This problem would exist for any higher-order function used in this manner within JavaScript:
// given the same `compose` function from above:
const result = [1, 2, 3, 4].map(
compose(
x => x + x,
x => x + '!!!',
x => parseInt(x)
)
);
console.log(result); // [2, 4, 6, 8]