TypeScript: Либо, либо для функций, передаваемых через несколько компонентов в React.

Я пишу приложение 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.

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

Экран вопросов.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;

ЭмоцииRater.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;

ЭмоцииRaterItem.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 является одной из двух функций и, следовательно, может быть вызвана?

EDIT: благодаря комментарию Estus я добавил сюда код вместо сути.


person J. Hesters    schedule 09.10.2018    source источник
comment
Вопрос должен содержать весь код, необходимый для понимания. Gists может уйти, но тело вопроса не может. Если фрагменты слишком велики, попробуйте уменьшить их, удалив ненужные части, см. stackoverflow.com/help/mcve. Если вы чувствуете, что показ всего в целом может быть полезен, подумайте о том, чтобы предоставить работающую демонстрацию, и stackblitz, и codeandbox предоставляют настройки React + TS.   -  person Estus Flask    schedule 09.10.2018
comment
@estus Я добавил код к вопросам. Спасибо за помощь!   -  person J. Hesters    schedule 09.10.2018


Ответы (1)


Если 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;
person Matt McCutchen    schedule 09.10.2018
comment
Спасибо, что нашли время и помогли мне. Я попробовал ваш код, и он работает. Но если написание универсального компонента, подобного этому, является плохой практикой, как еще можно создать повторно используемый компонент для более чем одного типа? - person J. Hesters; 10.10.2018
comment
Я бы сказал, что использование универсальных компонентов - это нормальная практика, если вы избегаете необоснованных случаев. Пока вы никогда не пишете метод рендеринга, который условно генерирует вызовы одного и того же универсального компонента с аргументами разных типов, все будет в порядке. (Вызовы от QuestionsScreen к EmotionsRater допустимы, потому что они безусловны.) Я думал о способе решения проблемы, но не реализовал его, и проблема возникает так редко, что я подозреваю, что многие разработчики не стали бы этого делать. заинтересованы в принятии дополнительных мер защиты, даже если они имеются. - person Matt McCutchen; 10.10.2018