问题
I'm writing a React Native application using TypeScript.
I have a component EmotionsRater
that accepts one of two types: Emotion or Need. It should also either accept a function of type rateNeed
or rateEmotion
. I combined these types to one called rateBoth
using the |
operator. And it passes this combined type down to another component called EmotionsRaterItem
. The problem is that EmotionsRaterItem
then claims:
Cannot invoke an expression whose type lacks a call signature. Type 'rateBoth' has no compatible call signatures.
I provided the boiled down code for all relevant components below.
QuestionsScreen.tsx:
// ... imports
export type rateEmotion = (rating: number, emotion: Emotion) => void;
export type rateNeed = (rating: number, emotion: Need) => void;
export interface Props {
navigation: NavigationScreenProp<any, any>;
}
export interface State {
readonly emotions: Emotion[];
readonly needs: Need[];
}
let EMOTIONS_ARRAY: Emotion[] = // ... some array of emotions
let NEEDS_ARRAY: Need[] = // ... some array of needs
export class QuestionsScreen extends Component<Props, State> {
static navigationOptions = // ... React Navigation Stuff
readonly state = {
emotions: EMOTIONS_ARRAY.slice(),
needs: NEEDS_ARRAY.slice()
};
swiper: any;
componentWillUnmount = () => {
// ... code to reset the emotions
};
toggleEmotion = (emotion: Emotion) => {
// ... unrelated code for the <EmotionsPicker />
};
rateEmotion: rateEmotion = (rating, emotion) => {
this.setState(prevState => ({
...prevState,
emotions: prevState.emotions.map(val => {
if (val.name === emotion.name) {
val.rating = rating;
}
return val;
})
}));
};
rateNeed: rateNeed = (rating, need) => {
this.setState(prevState => ({
...prevState,
need: prevState.emotions.map(val => {
if (val.name === need.name) {
val.rating = rating;
}
return val;
})
}));
};
goToIndex = (targetIndex: number) => {
const currentIndex = this.swiper.state.index;
const offset = targetIndex - currentIndex;
this.swiper.scrollBy(offset);
};
render() {
const { emotions, needs } = this.state;
return (
<SafeAreaView style={styles.container} forceInset={{ bottom: "always" }}>
<Swiper
style={styles.wrapper}
showsButtons={false}
loop={false}
scrollEnabled={false}
showsPagination={false}
ref={component => (this.swiper = component)}
>
<EmotionsPicker
emotions={emotions}
toggleEmotion={this.toggleEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={emotions.filter(emotion => emotion.chosen)}
rateEmotion={this.rateEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={needs}
rateEmotion={this.rateNeed}
goToIndex={this.goToIndex}
tony={true}
/>
</Swiper>
</SafeAreaView>
);
}
}
export default QuestionsScreen;
EmotionsRater.tsx:
// ... imports
export type rateBoth = rateEmotion | rateNeed;
export interface Props {
emotions: Emotion[] | Need[];
rateEmotion: rateBoth;
goToIndex: (targetIndex: number) => void;
tony?: boolean;
}
export interface DefaultProps {
readonly tony: boolean;
}
export class EmotionsRater extends PureComponent<Props & DefaultProps> {
static defaultProps: DefaultProps = {
tony: false
};
keyExtractor = (item: Emotion | Need, index: number): string =>
item.name + index.toString();
renderItem = ({ item }: { item: Emotion | Need }) => (
<EmotionsRaterItem emotion={item} rateEmotion={this.props.rateEmotion} />
);
renderHeader = () => {
const { tony } = this.props;
return (
<ListItem
title={tony ? strings.needsTitle : strings.raterTitle}
titleStyle={styles.title}
bottomDivider={true}
containerStyle={styles.headerContainer}
leftIcon={tony ? badIcon : goodIcon}
rightIcon={tony ? goodIcon : badIcon}
/>
);
};
goBack = () => {
this.props.goToIndex(0);
};
goForth = () => {
this.props.goToIndex(2);
};
render() {
return (
<View style={styles.container}>
<FlatList<Emotion | Need>
style={styles.container}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
data={this.props.emotions}
ListHeaderComponent={this.renderHeader}
stickyHeaderIndices={[0]}
/>
<ButtonFooter
firstButton={{
disabled: false,
onPress: this.goBack,
title: strings.goBack
}}
secondButton={{
disabled: false,
onPress: this.goForth,
title: strings.done
}}
/>
</View>
);
}
}
export default EmotionsRater;
EmotionsRaterItem.tsx:
// ... imports
export interface Props {
emotion: Emotion | Need;
rateEmotion: rateBoth;
}
export interface State {
readonly rating: number;
}
export class EmotionsRaterItem extends PureComponent<Props, State> {
readonly state = { rating: this.props.emotion.rating };
ratingCompleted = (rating: number) => {
this.setState({ rating });
this.props.rateEmotion(rating, this.props.emotion);
// This ^^^^^^^^^^^ throws the error mentioned in the post.
};
render() {
const { emotion } = this.props;
const { rating } = this.state;
const color = getColor(rating);
return (
<ListItem
title={emotion.name}
bottomDivider={true}
rightTitle={String(Math.round(rating * 100))}
rightTitleStyle={{ color: color.hex("rgb") }}
rightContentContainerStyle={styles.rightContentContainer}
subtitle={
<Slider
value={emotion.rating}
thumbTintColor={activeColor}
minimumTrackTintColor={color.hex("rgb")}
maximumTrackTintColor={color.alpha(0.4).hex("rgba")}
step={0.01}
onValueChange={this.ratingCompleted}
/>
}
/>
);
}
}
export default EmotionsRaterItem;
What is going on? Why doesn't TypeScript know that rateBoth
is one of two functions and therefore callable?
EDIT: Thanks to Estus's comment I added the code here instead of gists.
回答1:
If EmotionsRaterItem
has a function of type rateBoth
, then that function either requires an Emotion
or requires a Need
, but the caller does not know which of the type is required. Hence, under current TypeScript semantics, it's impossible to call the function. (You could imagine that maybe passing an argument that is both an Emotion
and a Need
should work, but TypeScript isn't that smart; see this issue.)
Instead, you could make EmotionsRater
and EmotionsRaterItem
generic in the type T
of the item they are working on (either Emotion
or Need
). (Of course, generic components are unsound in general, but it looks like the problem won't occur in your scenario.) Semi-complete example:
QuestionsScreen.tsx
// ... imports
import { Component } from "react";
import EmotionsRater from "./EmotionsRater";
import * as React from "react";
export interface Emotion {
emotionBrand: undefined;
name: string;
rating: number;
}
export interface Need {
needBrand: undefined;
name: string;
rating: number;
}
export type rateEmotion = (rating: number, emotion: Emotion) => void;
export type rateNeed = (rating: number, emotion: Need) => void;
export interface Props {
navigation: NavigationScreenProp<any, any>;
}
export interface State {
readonly emotions: Emotion[];
readonly needs: Need[];
}
let EMOTIONS_ARRAY: Emotion[] = []; // ... some array of emotions
let NEEDS_ARRAY: Need[] = []; // ... some array of needs
export class QuestionsScreen extends Component<Props, State> {
static navigationOptions; // ... React Navigation Stuff
readonly state = {
emotions: EMOTIONS_ARRAY.slice(),
needs: NEEDS_ARRAY.slice()
};
swiper: any;
componentWillUnmount = () => {
// ... code to reset the emotions
};
toggleEmotion = (emotion: Emotion) => {
// ... unrelated code for the <EmotionsPicker />
};
rateEmotion: rateEmotion = (rating, emotion) => {
this.setState(prevState => ({
...prevState,
emotions: prevState.emotions.map(val => {
if (val.name === emotion.name) {
val.rating = rating;
}
return val;
})
}));
};
rateNeed: rateNeed = (rating, need) => {
this.setState(prevState => ({
...prevState,
need: prevState.emotions.map(val => {
if (val.name === need.name) {
val.rating = rating;
}
return val;
})
}));
};
goToIndex = (targetIndex: number) => {
const currentIndex = this.swiper.state.index;
const offset = targetIndex - currentIndex;
this.swiper.scrollBy(offset);
};
render() {
const { emotions, needs } = this.state;
return (
<SafeAreaView style={styles.container} forceInset={{ bottom: "always" }}>
<Swiper
style={styles.wrapper}
showsButtons={false}
loop={false}
scrollEnabled={false}
showsPagination={false}
ref={component => (this.swiper = component)}
>
<EmotionsPicker
emotions={emotions}
toggleEmotion={this.toggleEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={emotions.filter(emotion => emotion.chosen)}
rateEmotion={this.rateEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={needs}
rateEmotion={this.rateNeed}
goToIndex={this.goToIndex}
tony={true}
/>
</Swiper>
</SafeAreaView>
);
}
}
export default QuestionsScreen;
EmotionsRater.tsx
// ... imports
import { PureComponent } from "react";
import * as React from "react";
import { Emotion, Need } from "./QuestionsScreen";
import EmotionsRaterItem from "./EmotionsRaterItem";
export interface Props<T extends Emotion | Need> {
emotions: T[];
rateEmotion: (rating: number, emotion: T) => void;
goToIndex: (targetIndex: number) => void;
tony?: boolean;
}
export interface DefaultProps {
readonly tony: boolean;
}
export class EmotionsRater<T extends Emotion | Need> extends PureComponent<Props<T> & DefaultProps> {
static defaultProps: DefaultProps = {
tony: false
};
keyExtractor = (item: Emotion | Need, index: number): string =>
item.name + index.toString();
renderItem = ({ item }: { item: T }) => (
<EmotionsRaterItem emotion={item} rateEmotion={this.props.rateEmotion} />
);
renderHeader = () => {
const { tony } = this.props;
return (
<ListItem
title={tony ? strings.needsTitle : strings.raterTitle}
titleStyle={styles.title}
bottomDivider={true}
containerStyle={styles.headerContainer}
leftIcon={tony ? badIcon : goodIcon}
rightIcon={tony ? goodIcon : badIcon}
/>
);
};
goBack = () => {
this.props.goToIndex(0);
};
goForth = () => {
this.props.goToIndex(2);
};
render() {
return (
<View style={styles.container}>
<FlatList<T>
style={styles.container}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
data={this.props.emotions}
ListHeaderComponent={this.renderHeader}
stickyHeaderIndices={[0]}
/>
<ButtonFooter
firstButton={{
disabled: false,
onPress: this.goBack,
title: strings.goBack
}}
secondButton={{
disabled: false,
onPress: this.goForth,
title: strings.done
}}
/>
</View>
);
}
}
export default EmotionsRater;
EmotionsRaterItem.tsx
// ... imports
import { PureComponent } from "react";
import * as React from "react";
import { Emotion, Need } from "./QuestionsScreen";
export interface Props<T extends Emotion | Need> {
emotion: T;
rateEmotion: (rating: number, emotion: T) => void;
}
export interface State {
readonly rating: number;
}
export class EmotionsRaterItem<T extends Emotion | Need> extends PureComponent<Props<T>, State> {
readonly state = { rating: this.props.emotion.rating };
ratingCompleted = (rating: number) => {
this.setState({ rating });
this.props.rateEmotion(rating, this.props.emotion);
};
render() {
const { emotion } = this.props;
const { rating } = this.state;
const color = getColor(rating);
return (
<ListItem
title={emotion.name}
bottomDivider={true}
rightTitle={String(Math.round(rating * 100))}
rightTitleStyle={{ color: color.hex("rgb") }}
rightContentContainerStyle={styles.rightContentContainer}
subtitle={
<Slider
value={emotion.rating}
thumbTintColor={activeColor}
minimumTrackTintColor={color.hex("rgb")}
maximumTrackTintColor={color.alpha(0.4).hex("rgba")}
step={0.01}
onValueChange={this.ratingCompleted}
/>
}
/>
);
}
}
export default EmotionsRaterItem;
来源:https://stackoverflow.com/questions/52716212/typescript-either-or-for-functions-passed-down-through-several-components-in-re