0

Given the following exercise:

type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R: never

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<string, MyReturnType<(v: boolean) => string>>>,
  Expect<Equal<123, MyReturnType<() => 123>>>,
  Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
  Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
  Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>,
]

type ComplexObject = {
  a: [12, 'foo']
  bar: 'hello'
  prev(): number
}

const fn = (v: boolean) => v ? 1 : 2
const fn1 = (v: boolean, w: any) => v ? 1 : 2

Even though the following types are inferred properly as I hover them,

MyReturnType<(v: boolean) => string>,

MyReturnType<typeof fn>,

MyReturnType<typeof fn1>

Expect generic does not evaluate true. The rest except the above types are correctly identified.

Lastly, if I replace unknown[] with any, it is resolved again.

The implementation of Equal of the library seems correct to me.

export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

Could you help me to figure out what's going on here?

ibrahim koz
  • 537
  • 4
  • 15

1 Answers1

2

The problem is that functions are contravariant with respect to the type of their parameters. Let's take a look at just the parameter types:

// true
type Args = [boolean] extends unknown[] ? true : false;

[boolean] extends unknown[], great! Now let's put these in a function type:

// false
type Args = ((...args: [boolean]) => void) extends ((...args: unknown[]) => void) ? true : false;

Uh oh! It's false now, even though all we did was wrap the parameter types in a function type! But wait, if we reverse it, we get true again?

// true
type Args = ((...args: unknown[]) => void) extends ((...args: [boolean]) => void) ? true : false;

What's going on here? The easiest way to explain this is to just imagine a scenario where we're doing this exact operation; assigning a (...args: [boolean]) => void to (...args: unknown[]) => void.

// type is the same as (...args: [boolean]) => void
const func = (arg: boolean) => console.log(arg);

function callWithRandomStuff(fn: (...args: unknown[]) => void) {
    fn(1, "hi", false);
}

callWithRandomStuff(func);

Do you realize what we just did? We are calling func with the wrong arguments! You cannot pass a function with narrower parameter types to something that expects a function with wider parameter types, since if you call the narrower function with arguments of a wider type, you just did something unsound.

This is why we have to use any. The purpose of any is to ignore the type system. If we use any[] here, we get true, as desired:

// true
type Args = ((...args: [boolean]) => void) extends ((...args: any[]) => void) ? true : false;

If you need more information on covariance and contravariance in TypeScript, I recommend this article explaining what it is and why types behave the way they do. You can also check out this answer which briefly touches on covariance, contravariance, invariance, and bivariance (yes, there's more).

Playground

kelsny
  • 23,009
  • 3
  • 19
  • 48
  • Although I was knowledgeable about covariance and contravariance, it had never even crossed my mind that it was applicable here. I'm agonizing over whether the following contravariance in functions would pose a problem in any situations. Thank you for the great explanation. – ibrahim koz Apr 03 '23 at 07:54