Reputation: 57974
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?
Upvotes: 3
Views: 84
Reputation: 33051
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
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
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
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
Upvotes: 3