Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript cannot infer type arguments correctly when swapping function parameters. #57287

Closed
webvv opened this issue Feb 4, 2024 · 2 comments

Comments

@webvv
Copy link

webvv commented Feb 4, 2024

🔎 Search Terms

It's been 2 years that I'm playing with higher order functions and kinds. This is part of my effort on creating a course of "Functional Programming with TypeScript". You can find the content here.
I'm working on a next video to teach "Functor composition", but I'm getting to a strange behavior on argument inference that I cannot reason about. You have an example of what I'm working on (compose function) in the docs here.
What baffles me is why swapping place of arguments in the compose function, makes my examples for higher kinded types fail. I've explained this completely in a question with a sample snippet of code and playground in StackOverFlow here. I'm trying this with the latest version of TypeScript.

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type inference.

⏯ Playground Link

Link

💻 Code

Unfortunately this is the smallest snippet of code I could shrunk the issue to:

type Option<A> = Some<A> | None

interface Some<A> {
  _tag: 'Some'
  value: A
}

interface None {
  _tag: 'None'
}

const some = <A,>(x: A): Option<A> => 
  ({ _tag: 'Some', value: x })

const none: Option<never> = 
  { _tag: 'None' }

const isNone = <A,>(x: Option<A>): x is None => 
  x._tag === 'None'

// --------------------

type Either<E, A> = Left<E> | Right<A>

interface Left<E> {
  _tag: 'Left'
  left: E
}

interface Right<A> {
  _tag: 'Right'
  right: A
}

const left = <E,A=never>(x: E):  Either<E, A> => ({ _tag: 'Left', left: x})
const right = <A,E=never>(x: A):  Either<E, A> => ({ _tag: 'Right', right: x})

const isLeft = <E, A>(a: Either<E, A>): a is Left<E> => 
  a._tag === 'Left'


// --------------------

interface URItoKind1<A> {
    'Option': Option<A>
}

interface URItoKind2<E,A> {
    'Either': Either<E,A>
}

type URIS1 = keyof URItoKind1<any>
type URIS2 = keyof URItoKind2<any, any>

type Kind1<URI extends URIS1, A> = URItoKind1<A>[URI]
type Kind2<URI extends URIS2, E, A> = URItoKind2<E,A>[URI]

type HKT1<URI, A> = { URI: URI; a: A }; 
type HKT2<URI, A, B> = { URI: URI; a: A; b: B }  

interface Functor1<F extends URIS1> {
    readonly URI: F
    map: <A, B>(f: (a: A) => B) => (fa: Kind1<F, A>) => Kind1<F, B>
}

interface Functor2<F extends URIS2> {
    readonly URI: F
    map: <E, A, B>(f: (a: A) => B) => (fa: Kind2<F, E, A>) => Kind2<F, E, B>
}

interface Functor<F> {
    readonly URI: F
    map: <A, B>(f: (a: A) => B) => (fa: HKT1<F, A>) => HKT1<F, B>
}

// --------------------------

const optionFunctor: Functor1<'Option'> = {
  URI: 'Option',
  map: <A,B>(f: (x: A) => B) => (fa: Option<A>): Option<B> => 
    isNone(fa) ? none : some(f(fa.value))
}

const eitherFunctor: Functor2<'Either'> = {
  URI: 'Either',
  map: <E,A,B>(f: (x: A) => B) => (fa: Either<E, A>): Either<E, B> => 
    isLeft(fa) ? fa : right(f(fa.right))
}

// ---------------------------

type Compose = <A, B, C>(
  f: (x: B) => C, 
  g: (x: A) => B
) => (x: A) => C

const compose: Compose = 
  (f, g) => x => f(g(x))

type ComposeR = <A, B, C>(
  g: (x: A) => B,
  f: (x: B) => C 
) => (x: A) => C

const composeR: ComposeR = 
  (g, f) => x => f(g(x))

// ---------------------------

type Increment = (x: number) => number
const increment: Increment = x => x+1

type ToStringg = (x: number) => string
const toStringg: ToStringg = x => `${x}`

const composed = compose(toStringg, increment)
const composedR = composeR(increment, toStringg)
composed(12)   // "13"
composedR(12)   // "13"

// This section compiles ok and types inferred correctly when composing functions.

// ---------------------------

const map1 = optionFunctor.map
const map2 = eitherFunctor.map

const composed1 = compose(map1, map2)       // <=== map2 has error and types cannot be inferred correctly
const composed2 = composeR(map1, map2)      // <=== map2 is ok here!

// Try switching map1 and map2. Why in `composed1` TypeScript cannot infer types correctly? how can I fix it?

🙁 Actual behavior

Type inference on compose function is not working as expected. This is from the snippet and sample code above.

// ...
const composed1 = compose(map1, map2)       // <=== map2 has error and types cannot be inferred correctly
const composed2 = composeR(map1, map2)      // <=== map2 is ok here!

🙂 Expected behavior

In both ways of composing map functions (map1, map2), typescript could infer generic type arguments correctly.

Additional information about the issue

This code is heavily influenced by fp-ts library. (I've already investigated the library. The library has a flow function that is implemented similar to the compose function in the TypeScript docs here, but there are no function like my compose function there that parameters are swapped.
The way I implemented the compose function and choosing the arguments order is based on the Composition-Operator in Math and Haskell for when we try to compose functions: (f o g in Math and f . g in Haskell means call g with the input parameter first and then call f with the output of g, and return)

@jcalz
Copy link
Contributor

jcalz commented Feb 4, 2024

Cross-linking to #30215, this is working as intended, perhaps this issue should be reworded as a feature request.

Duplicate of #31738

@webvv webvv closed this as completed Feb 5, 2024
@RyanCavanaugh
Copy link
Member

For future issues, please reduce your repros further

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants