TypeScript:或者通过React中的几个组件传递的函数

时间:2018-10-09 08:08:54

标签: reactjs typescript react-native typescript-typings typescript-types

我正在使用TypeScript编写React Native应用程序。

我有一个组件EmotionsRater,它接受​​两种类型之一:情感或需求。它还应该接受rateNeedrateEmotion类型的函数。我使用rateBoth运算符将这些类型组合为一个称为|的类型。并将此组合类型向下传递到另一个名为EmotionsRaterItem的组件。问题是EmotionsRaterItem然后声明:

Cannot invoke an expression whose type lacks a call signature. Type 'rateBoth' has no compatible call signatures.

我为下面的所有相关组件提供了简化的代码。

QuestionScreen.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;

这是怎么回事?为什么TypeScript不知道rateBoth是两个函数之一,因此可以调用?

编辑: 感谢Estus的评论,我在这里添加了代码,而不是要点。

1 个答案:

答案 0 :(得分:1)

如果EmotionsRaterItem具有类型rateBoth的函数,则该函数要么需要Emotion要么需要Need,但是调用者不知道是哪种类型是必须的。因此,在当前的TypeScript语义下,无法调用该函数。 (您可以想象,也许传递一个同时为{em> 和EmotionNeed的参数,但是TypeScript并不那么聪明;请参阅this issue。 )

相反,您可以使EmotionsRaterEmotionsRaterItem与其所处理项目的类型TEmotionNeed)通用。 (当然,generic components are unsound in general,但是看来您的情况不会出现问题。)半完整的示例:

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;