TypeScript: либо для функций, переданных через несколько компонентов в React - PullRequest
0 голосов
/ 09 октября 2018

Я пишу приложение React Native с использованием TypeScript.

У меня есть компонент EmotionsRater, который принимает один из двух типов: Emotion или Need.Также следует принять функцию типа rateNeed или rateEmotion.Я объединил эти типы с типом rateBoth, используя оператор |.И он передает этот комбинированный тип другому компоненту, называемому EmotionsRaterItem.Проблема заключается в том, что EmotionsRaterItem тогда заявляет:

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

Я предоставил свернутый код для всех соответствующих компонентов ниже.

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;

Что происходит?Почему TypeScript не знает, что rateBoth является одной из двух функций и, следовательно, может вызываться?

РЕДАКТИРОВАТЬ: Благодаря комментарию Estus я добавил код здесьвместо гист.

1 Ответ

0 голосов
/ 09 октября 2018

Если EmotionsRaterItem имеет функцию типа rateBoth, то для этой функции либо требуется Emotion, либо Need, но вызывающая сторона не знает, какой из типов требуется.Следовательно, в соответствии с текущей семантикой TypeScript невозможно вызвать функцию.(Вы можете представить, что, возможно, передача аргумента, который и , Emotion и Need, должен работать, но TypeScript не настолько умен, см. эту проблему .)

Вместо этого вы можете сделать EmotionsRater и EmotionsRaterItem универсальными в типе T элемента, над которым они работают (либо Emotion, либо Need).(Конечно, общие компоненты не работают в общем , но похоже, что проблема не возникнет в вашем сценарии.) Полу-полный пример:

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;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...