Andrew Li
Andrew Li

Reputation: 57964

Why does type have no overlap when in enum?

I'm trying to wrap my head around why TypeScript reports that my condition will always be false due to no type overlap between Action.UP | Action.DOWN and Action.LEFT in this situation (playground link):

class Component<S> {
    public state: S;
    public render() {}
}

enum Action {
    UP,
    DOWN,
    LEFT,
    RIGHT,
}

interface State {
    action: Action;
}

const initialAction: Action.UP | Action.DOWN = Action.UP;
class MyComponent extends Component<State> {
    public state = {
        action: initialAction,
    }

    public render() {
        // Why is action not of type Action as declared in the interface?
        const isLateral = this.state.action === Action.LEFT;
    }
}

If this.state.action only has an initial value of type Action.UP | Action.DOWN, but is declared as type Action in the interface, why can't I compare it to Action.LEFT? If I can reassign this.state.action in the future to be either LEFT or RIGHT, why is the condition always false?

Upvotes: 5

Views: 6380

Answers (3)

Nagibaba
Nagibaba

Reputation: 5358

in our case we forgot {} as const A: React.FC<IProps> = (first, second, ...rest) had to be const A: React.FC<IProps> = ({first, second, ...rest}). This is awful :)

Upvotes: 0

Shaun Luttin
Shaun Luttin

Reputation: 141552

Answer to "Why?"

Why is action not of type Action as declared in the interface?

It is of a different type because the child class is re-defining the state property and giving it a narrower type than it has in the parent class. If you are doing that by design, then you can access the broader interface of the parent class by casting inside render().

const isLateral = (this as Component<State>).state.action === Action.LEFT;

Recommended Approach

Alternatively, do what the React Component documentation says:

...if your component needs to use local state, assign the initial state to this.state directly in the constructor:

That is, do not re-redefine the parent class's state property; instead, use the already defined state property that extend gives you. Set its initial value in the constructor.

class MyComponent extends Component<State> {

    constructor() { 
        super();
        this.state = {
            action: initialAction,
        }
    }

    public render() {
        const isLateral = this.state.action === Action.LEFT;
    }
}

Upvotes: 3

Scrimothy
Scrimothy

Reputation: 2536

You have to type the public state to State like this:

public state: State = {
    action: initialAction,
}

Otherwise Typescript will infer that state.action is the same type as initialAction.

To answer the question in your comment

(playground link)

You redefined the value and type of state in your child class when you set as a new property.

My link illustrates that. I've added a new property on the parent class called count with a type number. If I define it in my child class as a new property, it infers it as any, and thus can be set to a string within render().

However, you can see how I handled your original state property. In order to use the parent's state type, you can call super() in the constructor and then define this.state.action = initialAction;.

That way, when you call render() it will be looking for the Action type on this.state.action, and not the newly defined Action.UP | Action.DOWN

Upvotes: 1

Related Questions