zedryas
zedryas

Reputation: 974

Typescript and concrete currying of overloded functions - how to?

I'm trying to write point free curried function in typescript. And doing so i do find an issue.
Typescript seems to be immediately evaluating type, and, in my understanding, do not allow very well curried function.

Let me illustrate by an example, first with non curried function

const fn = (x: number): string => null

function inline <T, U>(fn: (x: T) => U, src: T[]): U[]
function inline <T, U>(fn: (x: T) => U, src: Iterable<T>): Iterable<U>
function inline(fn: (x: any) => any, src: any): any {
  return null
}

const iarr = inline (fn, [1, 2]) // <-- string[]
const iset = inline (fn, new Set([1, 2])) // <-- Iterable<string>

Everything is in place and iarr and iset are correctly typed.

Now let's curry this function:

function pointfree <T, U>(fn: (x: T) => U): (src: T[]) => U[]
function pointfree <T, U>(fn: (x: T) => U): (src: Iterable<T>) => Iterable<U>
function pointfree(fn: (x: any) => any) {
  return (src: any): any => null
}

const curried = pointfree (fn) // <-- (src: number[]) => string[]

const parr = curried ([1, 2]) // <-- string[]
const pset = curried (new Set ([1, 2]))
//                    ^^^^^^^^^^^^^^^
// Argument of type 'Set<number>' is not assignable to parameter of
// type 'number[]'.

// or without caching same error
const pset = pointfree (fn) (new Set([1, 2]))

pset is evaluated as soon as pointfree (fn) is reached and is auto casted to the first type of the overloads which is an array of numbers, even without caching as curried

It's very common in fp to need to partially apply function... Thanks in advance

Upvotes: 0

Views: 209

Answers (1)

jcalz
jcalz

Reputation: 330571

Overloads are useful when different parameter types should result in different return types. It's not useful to have two different overload signatures with the same parameter types. That's because, as the handbook says:

[The compiler] looks at the overload list, and proceeding with the first overload attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload.

In your definition of pointfree, the parameter lists are identical for each overload. That means the compiler will always choose the first one if it chooses one at all.

So, what's the right way to do this? I suggest having a single call signature that returns an overloaded function. Like this:

// call signature, returns overloaded function
function pointfree<T, U>(fn: (x: T) => U): {
  (src: T[]): U[],
  (src: Iterable<T>): Iterable<U>
};
// implementation signature is the same as before 
function pointfree(fn: (x: any) => any) {
  return (src: any): any => null
}

Let's use it:

const curried = pointfree(fn)
// const curried: {
//  (src: number[]): string[];
//  (src: Iterable<number>): Iterable<string>;
// }

Now you can see that curried is an overloaded function with two call signatures. So the following calls behave as you expect:

const parr = curried([1, 2]) // <-- string[]
const pset = curried(new Set([1, 2])) // <-- Iterable<string>

Okay, hope that helps. Good luck!

Upvotes: 2

Related Questions