TypeScript: Either or for functions passed down through several components in React

空扰寡人 提交于 2019-12-13 03:40:08

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!