Reputation: 69
do you know why the Typescript compiler (4.3.5) works with the mergeAll1
method but fails on mergeAll2
?
The not expected result type Map2<T> & Map2<Base>
is not very handy :(
export type ActionReducerX<T> = () => T;
export type Map1<T> = {
[key in keyof T]: T[key];
};
export type Map2<T> = {
[key in keyof T]: ActionReducerX<T[key]>;
};
interface Base {
a: { aa: string };
}
type WithBase<T> = T & Base;
export function mergeAll1<T>(t: Map1<T>): Map1<WithBase<T>> {
const b: Map1<Base> = {
a: { aa: '' }
};
return {
...t,
...b
};
}
export function mergeAll2<T>(t: Map2<T>): Map2<WithBase<T>> {
const b: Map2<Base> = {
a: () => ({ aa: '' })
};
// error TS2322: Type 'Map2<T> & Map2<Base>' is not assignable to type 'Map2<WithBase<T>>'.
return {
...t,
...b
};
}
Upvotes: 1
Views: 81
Reputation: 33101
The main issue is in export type ActionReducerX<T> = () => T;
.
Seem to be that Map2<WithBase<T>>
and Map2<T> & Map2<Base>
are assignable to each other:
declare var returnType: Map2<WithBase<Value>>
declare var returnValue: Map2<Value> & Map2<Base>
returnType = returnValue
returnValue = returnType
But this is not the case when we use them with function argument type.
export function mergeAll2<T>(t: Map2<T>): Map2<WithBase<T>> {
const b: Map2<Base> = {
a: () => ({ aa: '' })
};
let x: Map2<T> & Map2<Base> = null as any as Map2<T> & Map2<Base>;
let y: Map2<WithBase<T>> = null as any as Map2<WithBase<T>>
x = y // ok
y = x // error
const result = {
...t,
...b
};
return result // error
}
Because function arguments are contravariant, we are no more allowed to assign x
to y
(Map2<T> & Map2<Base>
to Map2<WithBase<T>>
), The arrow of inheritance points in the opposite direction
.
There is a workaround:
export type ActionReducerX<T> = <U extends T>() => U;
export type Map2<T> = {
[key in keyof T]: ActionReducerX<T[key]>;
};
interface Base {
a: { aa: string };
}
type WithBase<T> = T & Base;
export function mergeAll2<T>(t: Map2<T>): Map2<WithBase<T>> {
// remove explicit type
const b = {
a: () => ({ aa: '' })
};
return {
...t,
...b
}
}
We can make this function contravariant
from the beginning.
export type ActionReducerX<T> = <U extends T>() => U;
type Animal = {
name: string
}
type Dog = {
paws: 4
} & Animal
type Check = Animal extends Dog ? true : false // false
type Check2 = ActionReducerX<Animal> extends ActionReducerX<Dog> ? true : false // true
Try to remove extra U
generic argument, and you will see that inheritance behavior will be changed in opposite direction.
I'm open to any sort of critics since this topic (variance) is still hard for me.
Here, here and here you can find more about this topic
UPDATE Custom wrapper for ActionReducer
Sadly it is not possible for me to extend the ActionReducer
You can just create your own custom type wrapper. See Wrapper
export type ActionReducerX<T> = () => T;
type Wrapper<T extends (...args: any) => any> = <U extends ReturnType<T>>() => U
export type Map2<T> = {
[key in keyof T]: Wrapper<ActionReducerX<T[key]>>;
};
interface Base {
a: { aa: string };
}
type WithBase<T> = T & Base;
export function mergeAll2<T>(t: Map2<T>): Map2<WithBase<T>> {
// remove explicit type
const b = {
a: () => ({ aa: '' })
};
return {
...t,
...b
}
}
type Animal = {
name: string
}
type Dog = {
paws: 4
} & Animal
type Check = Animal extends Dog ? true : false // false
type Check2 = Wrapper<ActionReducerX<Animal>> extends Wrapper<ActionReducerX<Dog>> ? true : false // true
Upvotes: 1