Reputation: 295
I want to use component inheritance, allowing common states in the abstract class and other ones in the inheritor. How do I combine this two types of states correctly? I am trying to Omit an external generic, but I still get a type error when i try to set states.
My code example:
import * as React from "react";
type CoreState = {
coreState1: number;
coreState2: string;
};
abstract class CoreComponent<OuterState = {}> extends React.Component<{}, Omit<OuterState, keyof CoreState> & CoreState> {
componentDidMount() {
this.setState({
coreState1: 1,
coreState2: "test",
});
}
}
type BaseState = {
state1: number;
state2: string;
};
class AnotherComponent<OuterState = {}> extends CoreComponent<Omit<OuterState, keyof BaseState> & BaseState> {
componentDidMount() {
super.componentDidMount();
this.setState({
state1: 1,
state2: "test",
});
}
render() {
return null;
}
}
Error i got:
TS2345: Argument of type '{ state1: 1; state2: "test"; }' is not assignable to parameter of type '(Pick<Pick<OuterState, Exclude<keyof OuterState, "state1" | "state2">> & BaseState, "state1" | "state2" | Exclude<Exclude<keyof OuterState, "state1" | "state2">, "coreState1" | "coreState2">> & CoreState) | ((prevState: Readonly<...>, props: Readonly<...>) => (Pick<...> & CoreState) | ... 1 more ... | null) | Pick<....'.
Types of property 'state1' are incompatible.
Type '1' is not assignable to type '(Pick<OuterState, Exclude<keyof OuterState, "state1" | "state2">> & BaseState)["state1"] | (Pick<Pick<OuterState, Exclude<...>> & BaseState, "state1" | ... 1 more ... | Exclude<...>> & CoreState)["state1"] | undefined'.
Error screenshot: https://yadi.sk/i/ByOr05JIauRyfQ
Upvotes: 0
Views: 2024
Reputation: 328262
The compiler unfortunately cannot perform the kind of higher-order type analysis on unspecified generic types necessary to conclude that what you are doing is type safe. Specifically, it would need to be able to verify the following for all T extends object
and U extends object
:
U extends Pick<Omit<T, keyof U> & U, keyof U> // 💻💥
It cannot do this when T
is an unspecified generic type, such as OuterState
inside the class body of CoreComponent<OuterState>
. And therefore has a problem with this.setState({ coreState1: 1, coreState2: "test" })
; if you substitute OuterState
for T
and CoreState
for U
, you get the offending comparison.
The closest I can find to canonical issue in GitHub for limitation is microsoft/TypeScript#28884. You may or may not want to go to that issue and give it a 👍 or explain your use case if you think it's compelling enough. But it's not obvious that this will ever be addressed, or when it would happen if so. For now it makes sense to just accept that this is a limitation in TypeScript.
Therefore, once you find yourself doing type manipulation on unspecified generic types, you will probably need to use type assertions or the like to overcome the deficiencies in the compiler's higher order type analysis.
For example, you could do this:
type Merge<T, U> = Omit<T, keyof U> & U;
type PickMerge<T, U> = Pick<Merge<T, U>, keyof U>;
abstract class CoreComponent<S = {}> extends React.Component<{}, Merge<S, CoreState>> {
componentDidMount() {
this.setState({
coreState1: 1,
coreState2: "test",
} as PickMerge<S, CoreState>); // assert here
}
}
I've created Merge
and PickMerge
so that you don't have to repeat yourself as much. The type assertion just says to the compiler that we are sure that the object literal is of type PickMerge<S, CoreState>
and that it shouldn't worry about verifying it. Similarly:
class AnotherComponent<S = {}> extends CoreComponent<Merge<S, BaseState>> {
componentDidMount() {
super.componentDidMount();
this.setState({
state1: 1,
state2: "test",
} as PickMerge<Merge<S, CoreState>, BaseState>); // assert here
}
}
You can see that this works, although it is starting to become tedious. You could always just say as any
and give up a little more type safety in exchange for convenience.
The other possibility would be some sort of refactoring, either of the emitted JS or of the types, but that strongly depends on your use cases. For example, you might be able to get away with saying that your component's state is a union of CoreState
and Merge<S, CoreState>
, so that the compiler is happy to allow a CoreState
to be specified:
abstract class CoreComponentAlternate<S = {}> extends
React.Component<{}, CoreState | Merge<S, CoreState>> {
componentDidMount() {
this.setState({
coreState1: 1,
coreState2: "test",
});
}
}
But of course this may interact poorly with the rest of your code base. If so, assertions are probably the best you will be able to achieve.
Upvotes: 2