3

In this code I have here:

type GETR<a extends string, b extends string> = [a,b]

interface Options<A extends string, B extends string> {
  bar: GETR<A,B>
}

function foo<A extends string, B extends string, C extends Options<A, B>>(r: C) {
  return r
}

const bar:GETR<"foo", "bar"> = null as any as GETR<"foo", "bar">

const x = foo({ bar })

There are no type errors. See it here

Now if all I change is the type of GETR from [a,b] to (k:a) => b with no other changes. Then the generic type params no longer get inferred and I have a type error:

type GETR<a extends string, b extends string> = (k:a) => b

interface Options<A extends string, B extends string> {
  bar: GETR<A,B>
}

function foo<A extends string, B extends string, C extends Options<A, B>>(r: C) {
  return r
}

const bar:GETR<"foo", "bar"> = null as any as GETR<"foo", "bar">

const x = foo({ bar })

Has a type error. See it here

I am trying to understand why, and what the best fix is?

JD Isaacks
  • 56,088
  • 93
  • 276
  • 422
  • Not exactly sure why inference does not work here however this https://www.typescriptlang.org/play?ssl=7&ssc=48&pln=7&pc=15#code/C4TwDgpgBA4gogFQEoB4CGUIA9gQHYAmAzlEcAE4CWeA5gDRQBGmO+xpF1NAfFALxQAFAGsAXGgCU-XowBQs6rnIAzNAGNoAeTDBKAezxEUAQRa5CJMlVoMAQmbaXOtXgG9ZUJmnKjYiVMZ0ttyyAL7yygCueGq6BlDKenomDhYc1vRQ9tjm7FZc3II+UNpxhiZ23FLunuQQwJHkeFDkYfJqBmRePvDIKABEiXr9DP2M3v28AniRADazUGgkaHggiyS9qINJI1BjEyGyHYbAUFj8CUmCrt1QoRJAA seems to work – shantr Sep 15 '22 at 15:12
  • Can't really "fix" this, more of "rewrite" this, but it doesn't work, because `C` is inferred as `Options`, which means `bar` should be of type `GETR`. You have given it `GETR<"foo", "bar">` instead. You might think that it's okay, but remember who's calling who here. Since you are giving it a *more specific type* than what it expects, it raises an error from TypeScript. You can't safely assign a `string` to `"foo"`. Only `"foo"` is assignable to `"foo"`. – kelsny Sep 15 '22 at 15:37
  • @caTS I understand that C is inferred as `Options` but the question is "why?" when in a very similar scenario that I shared it is correctly inferred as `Options<"foo" "bar">` ... I would expect the inference to be the same in both cases, so why is it different when the type ultimately resolves to a function? – JD Isaacks Sep 15 '22 at 15:57
  • I will just say that (a: A) => B and [A, B] are not "very similar", and that completely different rules of param inference, widening, etc. are applied to a function param. Which combination in the interface vs type causes the widening param? I don't know, but it helps to see that generic function param inference is not a trivial change! – Yuji 'Tomita' Tomita Sep 18 '22 at 15:29

1 Answers1

3

I believe this is because contravariance. However, I am not 100% sure, because it also might be invariance. There is an easy fix, just get rid of C generic argument:

type Fn<Arg extends string, Return extends string> = (k: Arg) => Return

interface Options<Arg extends string, Return extends string> {
  prop: Fn<Arg, Return>
}

function foo<
  Arg extends string,
  Return extends string,
>(r: Options<Arg, Return>) {
  return r
}

declare const prop: Fn<"foo", "bar">

const x = foo({ prop }) // ok

Playground

Consider this small example:

declare let stringString: Options<string, string>
declare let fooBar: Options<'foo', 'bar'>

stringString = fooBar // error
fooBar = stringString // error

As you might have noticed: stringString and fooBar are not assignable to each other.

The problem is in Arg generic type (in your example it is a). If you get rid of Arg (a), your example will compile:

type Fn<Return extends string> = () => Return

type Options<Return extends string> = {
  prop: Fn<Return>
}

function foo<
  Return extends string,
  C extends Options<Return>
>(r: C) {
  return r
}

declare const prop: Fn<"bar">

const x = foo({ prop }) // no error

It compiles, because Return is in covariant position.

Let's try to add back Arg but get rid of Return:

type Fn<Arg extends string> = (arg: Arg) => void

type Options<Arg extends string> = {
  prop: Fn<Arg>
}

function foo<
  Arg extends string,
  C extends Options<Arg>
>(r: C) {
  return r
}

declare const prop: Fn<"foo">

const x = foo({ prop }) // still error

declare let stringPrimitive: string;
declare let fooBarPrimitive: 'bar'

stringPrimitive = fooBarPrimitive // ok
fooBarPrimitive = stringPrimitive  // error

declare let stringString: Options<string>
declare let fooBar: Options<'bar'>

stringString = fooBar // error
fooBar = stringString // ok


Playground

There is still an error. Please check assignability of stringPrimitive , fooBarPrimitive, stringString and fooBar.

In first example, fooBarPrimitive literal type is assignable to string and it is expected. However, in second example, fooBar is no more assignable to stringString because of contravariance. Function arguments are in contravariant positions.

Let's try to add Return generic (in your example it is b):

type Fn<Arg extends string, Return extends string> = (arg: Arg) => Return

type Options<Arg extends string, Return extends string> = {
  prop: Fn<Arg, Return>
}

function foo<
  Arg extends string,
  Return extends string,
  C extends Options<Arg, Return>
>(r: C) {
  return r
}

declare const prop: Fn<"foo", 'bar'>

const x = foo({ prop }) // still error

declare let stringString: Options<string, string>
declare let fooBar: Options<'foo', 'bar'>

stringString = fooBar // error
fooBar = stringString // error

Playground

Please check assignability of stringString and fooBar, they are no more assignable to each other at all. In both cases you have got an error.

From what I understood, adding C generic triggers contravariant behavior of Arg generic.

This is not the first time I have faced such behavior.

Consider this example, but please turn off strictFunctionTypes flag:

type Animal = { tag: 'animal' }

type Dog = Animal & { bark: true }
type Cat = Animal & { meow: true }

declare let animal: (x: Animal) => void;
declare let dog: (x: Dog) => void;
declare let cat: (x: Cat) => void;

animal = dog; // ok without strictFunctionTypes and error with

dog = animal; // should be ok

dog = cat; // should be error

dog is assignable to animal but should not be.

Now, try add generic to animal function:

type Animal = { tag: 'animal' }
type Dog = Animal & { bark: true }

// generic is here
declare let animal: <T extends Animal>(x: T) => void;
declare let dog: (x: Dog) => void;

animal = dog; // error even without strictFunctionTypes

Now, dog is not assignable to animal even without strictFunctionTypes flag. I did not find an explanation of this behavior in docs.

See corresponding article here

Please check this answer if you are interested in *-variance topic.

P.S. I will be happy if somebody will confirm or critic my thoughts, I was trying my best

  • 1
    First I want to say, if you are still in Ukraine and taking the time to answer my questions, I don't even know what to say - but I salute you. Thanks and hope all is well :) Second, I've never even heard of the term contravariant before, so thanks for pointing me to that informative post. – JD Isaacks Sep 19 '22 at 17:54
  • 1
    @JDIsaacks, stackoverflow helps me to unwind . You are welcome. Apart from that, your typescript questions are always interesting )) – captain-yossarian from Ukraine Sep 19 '22 at 18:46
  • Btw, [this](https://twitter.com/TitianCernicova/status/1571856917177442305) is what @Titian Cernicova-Dragomir said about this answer . So, now we know for sure that the problem is in `*-variance`, however, still unable to explain behavior of `C` generic – captain-yossarian from Ukraine Sep 20 '22 at 06:54