简体   繁体   中英

How do I make a generic constraint for a superset of a discriminated union

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);

Playground Link

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);

Playground link

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);

Playground link

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM