TypeScript:通过 React 中的多个组件向下传递的函数或者
TypeScript: Either or for functions passed down through several components in React
我正在使用 TypeScript 编写 React Native 应用程序。
我有一个组件 EmotionsRater
接受以下两种类型之一:情感或需求。它还应该接受类型为 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 的评论,我在这里添加了代码而不是要点。
如果 EmotionsRaterItem
具有 rateBoth
类型的函数,则该函数需要 Emotion
或需要 Need
,但调用者不知道是哪一个类型是必需的。因此,在当前的 TypeScript 语义下,不可能调用该函数。 (你可以想象,也许传递一个 both 一个 Emotion
和一个 Need
的参数应该有效,但 TypeScript 不是那么聪明;见 this issue.)
相反,您可以使 EmotionsRater
和 EmotionsRaterItem
在他们正在处理的项目的类型 T
中通用(Emotion
或 Need
). (当然是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;
我正在使用 TypeScript 编写 React Native 应用程序。
我有一个组件 EmotionsRater
接受以下两种类型之一:情感或需求。它还应该接受类型为 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 的评论,我在这里添加了代码而不是要点。
如果 EmotionsRaterItem
具有 rateBoth
类型的函数,则该函数需要 Emotion
或需要 Need
,但调用者不知道是哪一个类型是必需的。因此,在当前的 TypeScript 语义下,不可能调用该函数。 (你可以想象,也许传递一个 both 一个 Emotion
和一个 Need
的参数应该有效,但 TypeScript 不是那么聪明;见 this issue.)
相反,您可以使 EmotionsRater
和 EmotionsRaterItem
在他们正在处理的项目的类型 T
中通用(Emotion
或 Need
). (当然是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;