When defining a generic class Foo<X>
, where X is intended to be a discriminated union type, is there a way to express "X must be a superset of the discriminated union Y"?
I have a situation where I am using a discriminated union to represent different action types. The real-world context of this is an application using Redux, so actions are each different types with different payloads, and the Redux reducer is a function that can accept any of the actions, so I use a discriminated union of the action types to describe the action parameter.
In the example below, which resembles my real-world problem, I have an extensible base class which knows how to handle the BaseActionTypes
, and I want to be able to pass in ExtendedTypes
as the generic parameter
interface Run {
}
interface Walk {
}
type BaseActionTypes = Run | Walk
interface Jump {
}
type ExtendedActionTypes = BaseActionTypes | Jump;
class ActionDoer<ActionTypes extends BaseActionTypes> {
doAction(a: ActionTypes) {
}
walk() {
const w: Walk = {};
this.doAction(w); // ERROR!
}
}
class ExtendedActionDoer extends ActionDoer<ExtendedActionTypes> {
}
const extendedActionDoer = new ExtendedActionDoer();
const j: Jump = {};
extendedActionDoer.doAction(j);
My code generates the error:
Argument of type 'Walk' is not assignable to parameter of type 'ActionTypes'.
'Walk' is assignable to the constraint of type 'ActionTypes', but 'ActionTypes' could be instantiated with a different subtype of constraint 'BaseActionTypes'.(2345)
I'm not too clear on why the base ActionDoer
can't doAction(w)
here. I'm trying to add a constraint that says "whatever union of actions is passed in as ActionTypes
, it must include at least the set of actions in the union BaseActionTypes
, and can possible include other actions. Or, said another way, ActionTypes
must be a superset of BaseActionTypes
I think maybe ActionTypes extends BaseActionTypes
might not be the correct kind of constraint here for what I want to do? Initially extends
seemed right because the ExtendedActionTypes
is an "extension" of the BaseActionTypes
, but thinking about this in terms of class inheritance makes me realize that extends
is probably not the correct way of doing this. (ie if class A extends class B, then A has all the fields in B, plus more. Whereas that relationship is not true between ExtendedActionTypes
and BaseActionTypes
.
Is there a better way to express a constraint on a discriminated union to say "X must by a superset of Y" for two discriminated union types X and Y?
How the generic type works here with extend
is that we are constraining that our class finally will be containing one or many of possible members from BaseActionTypes
. For union types extends means - everything assignable to the type, it means it can have the same amount of variants or less, but nothing more. You ask how is then your type ExtendedActionTypes
assignable if it has one option more.. and really it is not, it is only because you didn't fill the implementation of types, and all three are the same thing as TS is structurally typed language. If you add to these types and properties you will have an error, check this here .
So ExtendedActionTypes
is not assignable to BaseActionTypes
because it has more options not less.
Your error though has different reason, as you are trying to set value of type Walk
into value which ActionTypes extends BaseActionTypes
. And it means the ActionTypes
is possible type which doesn't include Walk
, so you cannot perform such assignment. Below the proove:
// no error as `Run` extends `BaseActionTypes`
class ExtendedActionDoer extends ActionDoer<Run> {
}
As you can see Walk
cannot be assigned to Run
.
One of possible issue fix is removing the constraint from the class at all:
type ExtendedActionTypes = BaseActionTypes | Jump;
class ActionDoer {
doAction<ActionType extends BaseActionTypes>(a: ActionType) {
}
walk() {
const w: Walk = {type: 'Walk'};
this.doAction(w);
}
}
class ExtendedActionDoer extends ActionDoer {
doAction(a: ExtendedActionTypes) {
}
}
const extendedActionDoer = new ExtendedActionDoer();
const j: Jump = {type: 'Jump'};
extendedActionDoer.doAction(j);
We can also keep the generic but requite to have properties, consider:
type ExtendedActionTypes = Jump;
class ActionDoer<ActionType> {
doAction(a: ActionType | BaseActionTypes) {
}
walk() {
const w: Walk = {type: 'Walk'};
this.doAction(w);
}
}
class ExtendedActionDoer extends ActionDoer<ExtendedActionTypes> {
doAction(a: ExtendedActionTypes) {
}
}
const extendedActionDoer = new ExtendedActionDoer();
const j: Jump = {type: 'Jump'};
extendedActionDoer.doAction(j);
Most important part is - doAction(a: ActionType | BaseActionTypes)
I am saying whatever you give me there I will take, but there always will be members of BaseActionTypes
.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.