2

Looking at the Ramda documentation for transduce, there are two examples given, each of which cause the Typescript compiler to throw a different error.

Example 1:

test('ex. 1', () => {
  const numbers = [1, 2, 3, 4]

  const transducer = compose(
    map(add(1)),
    take(2)
  )

  const result = transduce(transducer, flip(append), [], numbers)

  expect(result).toEqual([2, 3])
})

Typescript throws the following exception for flip(append):

Argument of type '(arg1: never[], arg0?: {} | undefined) => <T>(list: readonly T[]) => T[]' is not assignable to parameter of type '(acc: ({} | undefined)[], val: {} | undefined) => readonly ({} | undefined)[]'.
      Types of parameters 'arg1' and 'acc' are incompatible.
        Type '({} | undefined)[]' is not assignable to type 'never[]'.
          Type '{} | undefined' is not assignable to type 'never'.
            Type 'undefined' is not assignable to type 'never'.

If I change flip(append) to flip(append) as any the code works as expected.

Example 2:

test('ex. 2', () => {
  const isOdd = x => x % 2 === 1
  const firstOddTransducer = compose(
    filter(isOdd),
    take(1)
  )

  const result = transduce(
    firstOddTransducer,
    flip(append) as any,
    [],
    range(0, 100)
  )

  expect(result).toEqual([1])
})

Typescript throws the following exception for firstOddTransducer:

Argument of type '(x0: readonly any[]) => Dictionary<any>' is not assignable to parameter of type '(arg: any[]) => readonly any[]'.
      Type 'Dictionary<any>' is missing the following properties from type 'readonly any[]': length, concat, join, slice, and 16 more.

Same as above, if I change firstOddTransducer to firstOddTransducer as any the code works as expected.

First, what do these particular errors even mean?

Second, what is the best way to deal with these sorts of issues with functional typescript? So often, when looking at various learning resources for typescript, users are warned against using any or against using // @ts-ignore as if it something you should never do, but the more complex my codebase gets, and the more functional my programming style becomes, the more of these seaming incomprehensible errors messages I receive for perfectly acceptable code. I don't mind spending a little bit of my time to make the types better, but I don't want to spend too much time debugging problems with types when I know the code is good.

Third, are there any tips that can help in situations when you're not quite sure if there is a problem with the types or with typescript, as above, or if there is a problem with the javascript code, e.g., techniques for determining where the actual problem is so you can either investigate or ignore?

RichardForrester
  • 1,048
  • 11
  • 19
  • it seems like ramda isn't up to date for working with the latest versions of typescript. You can mess with the strictness of type catching inyour ts config or do the things you're talking about (abusing ignore or any types). These things are advised against because the point of typescript is to provide typings. the maintainers of ramda should be doing a better job of keeping up to date, or you should look into a better kept lib. – bryan60 May 27 '19 at 19:24
  • I don't think it's fair to put all the blame on ramda, Typescript just barely got some support for "higher order type inference from generic functions" in the latest v.3.4 update [notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html). Lodash has similar issues, despite being more popular and having less of an emphasis on fp point-free style; in fact, lodash doesn't support transducers as far as I know. I am taking a closer look at [Sanctuary](https://sanctuary.js.org/) though... – RichardForrester May 27 '19 at 19:42
  • maintaining a typings file is part of maintaining a web lib in this era – bryan60 May 27 '19 at 19:45
  • @bryan60: if you're volunteering, the Ramda team would welcome the help. The last time I checked, Typescript wasn't expressive enough to support all of Ramda's functions. We could also use help with Flow, ClojureScript, Elm, and 3489 other compile-to-Javascript languages. – Scott Sauyet May 27 '19 at 20:22
  • @ScottSauyet not volunteering. I've got my own job to do and my own projects to maintain. – bryan60 May 27 '19 at 20:27
  • @RichardForrester: The Ramda team has no internal expertise in Typescript typings. You probably will need to check with those who maintain the typings files you use. – Scott Sauyet May 27 '19 at 21:56
  • @ScottSauyet Thanks, I thought Ramda installed with it’s own typings. Anyway I’m perfectly happy with Ramda, it’s been the best so far of all the FP libs I’ve tried, just looking for some ADTs to go along with it. I just get really frustrated with TS every so often when I see all the “as any” and “ignore”s everywhere. – RichardForrester May 28 '19 at 06:49
  • @RichardForrester: I have the same frustrations with TS every time I try to use it. So I try to use it as little as possible. I know it's popular and all, but it doesn't do anything for me. – Scott Sauyet May 28 '19 at 12:20
  • 1
    @ScottSauyet I definitely see a lot of benefits from TS with my main project that’s close now to 100k lines of code. Refactoring without TS would be very difficult, but I am looking at different options, and there seem to be new options popping up every day. I’m starting to learn some haskell with the hope that by the time I’m proficient, there will be reasonable interoperability with JS or web assembly - gotta have hope! – RichardForrester May 28 '19 at 13:36

1 Answers1

1

As of writing, this is a limitation of TypeScript, there's simply no way for you to express that part of the type has to be inferred from later use.

It isn't until you call transduce that you actually bind all the type parameters so, it isn't a complete type until then, or, rather, TypeScript doesn't know how to complete the type until you do that. TypeScript will attempt to infer the type from context, so maybe, if you put all that on a single line it might be able to do that (not saying it will).

Fundamentally a transducer is an abstraction of a sequence/stream/observable and you grant the compiler this information when you actually run your transducer over some data. That's when it needs to bind the type information of the data.

The only thing I can think of here is that you stick unknown as a placeholder type of the data until you run transduce or into. It won't grant you type safety but it will suppress the compiler warnings which either way are false positives. It's not perfect but it should work.

John Leidegren
  • 59,920
  • 20
  • 131
  • 152