Definitelytyped: @types/ramda - Typings do not work for pipe function using R.filter

Created on 7 May 2018  路  17Comments  路  Source: DefinitelyTyped/DefinitelyTyped

Authors: @donnut @mdekrey @mrdziuban @sbking @afharo @teves-castro @1M0reBug @hojberg @charlespwd @samsonkeung @angeloocana @raynerd @googol @moshensky @ethanresnick @leighman @CaptJakk

Hi. I'm trying to use R.pipe with R.filter, but I couldn't get the typing to work. But if I ignore the typings by adding // @ts-ignore the code works. I'm not asking this on StackOverflow because I'm almost sure it's a bug on the typings.

This doesn't work (the typings, the code is great, as ignoring the error part with // @ts-ignore makes it work

const searchRegexp = /\.dev\.test\.io/;
const env = 'prod';
const data = [
  'https://testing.dev.test.io',
  'https://testing2.local.dev.test.io',
  'https://testing3.dev.test.io',
];

const values = pipe<string[], string[], string[], string[]>(
  map(v => v.trim()),

  /*
    Below line throws:

    Argument of type 'Filter<{}>' is not assignable to parameter of type '(x: string[]) => string[]'.
      Type '{}[]' is not assignable to type 'string[]'.
      Type '{}' is not assignable to type 'string'. (2345)
  */
  filter(v => !(contains('.local.', v) && env !== 'dev')),

  map(v => v.replace(searchRegexp, env === 'prod' ? '.test.com' : `.${env}.test.io`)),
)(data.split(','));

This works (both typings & code)

const searchRegexp = /\.dev\.test\.io/;
const env = 'prod';
const data = [
  'https://testing.dev.test.io',
  'https://testing2.local.dev.test.io',
  'https://testing3.dev.test.io',
];

const values = pipe<string[], string[], string[], string[]>(
  map(v => v.trim()),
  // adding this extra anonymous function here, makes everything to work
  list => filter(v => !(contains('.local.', v) && env !== 'dev'), list),
  map(v => v.replace(searchRegexp, env === 'prod' ? '.test.com' : `.${env}.test.io`)),
)(data.split(','));

Most helpful comment

This is also a big problem for us, even very simple things like:

import { pipe, filter } from 'ramda';

const input = [1,2,3,4];

const output = pipe(
  filter((n: number) => n % 2 === 0)
)(input);

do not compile:

Argument of type 'number[]' is not assignable to parameter of type 'Dictionary<number>'.
  Index signature is missing in type 'number[]'.

The current typing for for filter is:

filter<T>(fn: (value: T) => boolean): Filter<T>;
filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
filter<T>(fn: (value: T) => boolean, obj: Dictionary<T>): Dictionary<T>;

For arrays the following typing is already fixing the issue:

filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
filter<T>(fn: (value: T) => boolean): (list: ReadonlyArray<T>) => T[];

This problem renders usage of ramda in TS pretty much unusable in some situations...

All 17 comments

I'm not sure this is a bug in the typing so much as a deficiency in the type inference. I can look into what is causing this but there is a chance you may have to parameterize filter.

Any response on this? I really think that I'm not using the types incorrectly.

This is also a big problem for us, even very simple things like:

import { pipe, filter } from 'ramda';

const input = [1,2,3,4];

const output = pipe(
  filter((n: number) => n % 2 === 0)
)(input);

do not compile:

Argument of type 'number[]' is not assignable to parameter of type 'Dictionary<number>'.
  Index signature is missing in type 'number[]'.

The current typing for for filter is:

filter<T>(fn: (value: T) => boolean): Filter<T>;
filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
filter<T>(fn: (value: T) => boolean, obj: Dictionary<T>): Dictionary<T>;

For arrays the following typing is already fixing the issue:

filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
filter<T>(fn: (value: T) => boolean): (list: ReadonlyArray<T>) => T[];

This problem renders usage of ramda in TS pretty much unusable in some situations...

Obviously, the same applies for the reject function and similar fix works too:

reject<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
reject<T>(fn: (value: T) => boolean): (list: ReadonlyArray<T>) => T[];

Here's a proposal:
We add a second type parameter to filter and reject, that specifies whether the list or object input is expected. The list is probably most commonly used so we could make that the default.

type Dictionary<T> = Record<string, T>;

type Filter<T, kind extends 'list' | 'dictionary'> = kind extends 'list' ? (list: ReadonlyArray<T>) => T[]
    : kind extends 'dictionary' ? (obj: Dictionary<T>) => Dictionary<T>
    : never;

declare function filter<T, kind extends 'list' | 'dictionary' = 'list'>(fn: (value: T) => boolean): Filter<T, kind>;
declare function filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
declare function filter<T>(fn: (value: T) => boolean, obj: Dictionary<T>): Dictionary<T>;
declare function pipe<T1, T2, R>(f1: (t: T1) => T2, f2: (t: T2) => R): (t: T1) => R;

pipe(
    filter((val: string) => val.length > 2),
    (x) => x.length
)(['list', 'or', 'object'])

pipe(
    filter<string, 'dictionary'>((val) => val.length > 2),
    (x) => x.length
)({ a: 'list', b: 'or', c: 'object' })

I have a branch for this here: https://github.com/googol/DefinitelyTyped/tree/ramda/filter-fix but I'll need to check the tests before making a PR. There seem to be tests that should catch the original problem already, but they aren't most likely making the correct assertions.

@googol Hey,

if I understand correct, your change merged with #28592 breaks the following "hack":

declare module 'ramda' {
  interface Static {
    filter<T>(fn: (value: T) => boolean): (list: ReadonlyArray<T>) => T[];
    filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
  }
}

Error:

.../ramda.d.ts:20:5 - error TS2300: Duplicate identifier 'filter'.

20     filter<T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];

Of course I can fix the issue now via removing my custom types and adding the Kind generic, but this is a breaking change not everyone is willing to make I suppose.

Are my assumptions correct? Do you see any way to fix this problem?

Honestly, to me personally that the situation is now worse than before the #28592. Do not get me wrong, it is really nice that people care about this project and maintain the types, so thanks for the contribution! I just wanted to point out potential problems with the current solution. What is you opinion on that?

Update: what I would basically like to have is the following code compiling:

import { filter, pipe } from 'ramda';

const arr = [0, 1, 2, 3];

const fn = pipe(
  filter(x => !!x)
);

fn(arr);

which worked before with the mentioned "hack".

@bzums you'd have to override Filter interface, not methods on Static.

import R from 'ramda';

declare module 'ramda' {
    interface Filter {
      /* overrides */
    }
}

Oh yeah of course, thanks!
So for the sake of documentation, this solves our issue currently:

declare module 'ramda' {
  interface Filter {
    <T>(fn: (value: T) => boolean): (list: ReadonlyArray<T>) => T[];
    <T>(fn: (value: T) => boolean, list: ReadonlyArray<T>): T[];
  }
}

I'm running into this problem also, but not entirely sure how the fix works. It seems once you call filter(fn) it returns a new function with interface FilterOnceApplied<T>. The new interface is for a function overloaded with 2 method signatures:

interface FilterOnceApplied<T> {
  (list: ReadonlyArray<T>): T[];
  (obj: Dictionary<T>): Dictionary<T>;
}

So far as I can tell, the first signature is not causing any issue. It's only the second that causes problems. If I remove the second signature from the interface, the ts error vanishes.

Anyone with a good understand of the mechanics at play here able to help me understand how this works?

For anyone having this problem, I solved it with out overriding ramda's reject interface, I just did this:

reject<string, 'object'>(propEq('id', id)),

Actually, there is a type helper: <T, Kind extends 'array'>(fn: (value: T) => boolean): (list: readonly T[]) => T[];

Filter used like this should have a correct return type: pipe(filter<MyType, 'array'>(myFunction))(myArray). This is because filter is supposed to work with both arrays and objects, but typescript gets confused as a result.

I've found this issue because I've found current typings for filter and reject do not fully support type guards. It would be good to extend existing types with support for filtering functions which are type guards and should narrow the result type. I've wrote sample typings for a use case with array:

type ArrayFilter = <T2 extends T1, T1>(fn: (value: T1) => value is T2) => (list: readonly T1[]) => T2[];
type ArrayReject = <T2 extends T1, T1>(fn: (value: T1) => value is T2) => (list: readonly T1[]) => Exclude<T1, T2>[];

I did not test them thoroughly though.

where is this type helper @vincaslt ?

Creating a ramda.d.ts type definition file with the following:
```
declare namespace R {
interface Static {
filter(fn: (value: T) => boolean): (list: ReadonlyArray) => T[]
filter(fn: (value: T) => boolean, list: ReadonlyArray): T[]
}
}
````

worked for us. Allowing us to do:
export const getCount = <T extends any>(fn: (value: T) => boolean) => pipe(filter<T>(fn), length)

instead of the more verbose
export const getCount = <T extends any>(fn: (value: T) => boolean) => pipe<readonly T[], readonly T[], number>(filter<T>(fn), length)

Hey, I gave usage example in my comment above, but here's the actual definition: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ramda/tools.d.ts#L217

This approach no longer works for 0.27.0. Filter and reject now appear to be defaulting to the FilterOnceApplied signature which has no notions of readonly collections. I'm not sure if this is the correct case to mention this one, but it seemed the most relevant

I tried to make simplest pipe function:

const takeLast10 = pipe(reverse, take(10))

And got this error:

Screenshot 2020-07-19 at 06 17 50

It seems the issue is related to pretty much any function which can be used with more than one type, like reverse can be used for both arrays and strings.

And filter can be used with objects and arrays.

It's not excuse for ramda I suppose, but it seems to be the reason.

Was this page helpful?
0 / 5 - 0 ratings