Я пытаюсь определить тип машинописного текста для реквизита компонента реакции. Мой компонент — это базовая кнопка, которая принимает либо опору icon, либо опору text. Он не может иметь оба, но должен иметь один.
Я пытался начать с базового размеченного союза, но он не работает так, как ожидалось:
interface TextButtonProps extends TypedButtonProps {
text: string
}
interface IconButtonProps extends TypedButtonProps {
icon: JSX.Element
}
export const Button = ({ onClick, ...props }: IconButtonProps | TextButtonProps): JSX.Element => {
//...
Когда я использую этот компонент в другом месте, TS не выдает никаких ошибок:
<Button icon = {<IconClose />} text='test' uiVariant='default' />
Следуя статье, которую я нашел в Интернете, описывающей интерфейсы с необязательными свойствами, которые никогда не работают:
interface TextButtonProps extends TypedButtonProps {
text?: string
icon?: never
}
interface IconButtonProps extends TypedButtonProps {
icon?: JSX.Element
text?: never
}
Все мои варианты использования <Button> будут вызывать ошибку, если существуют и icon, и text.
Почему это работает? Я не в восторге от того, насколько это многословно — если я добавлю больше типов кнопок, мне придется добавить эти новые свойства в каждый отдельный интерфейс.
Моя вторая проблема заключается в том, что, поскольку свойства являются необязательными, я могу избежать определения ни значка, ни текстовой поддержки - помните, что мне нужно убедиться, что одно или другое существует.
Есть ли более чистое решение, которое удовлетворило бы мои потребности?
Может что-то вроде этого?
FWIW, это не совсем конкретный вопрос React, у вас может возникнуть проблема с вызовом любой функции, которая принимает тип объединения.
Вам не нужно определять два интерфейса, чтобы установить значения, которые получит ваш компонент, достаточно одного интерфейса. Укажите, какие параметры вам нужны. в случае значка, который является названием строки типа, также заголовок кнопки типа строка; затем объявите значения, которые не потребуются, и инициализируйте их в своем компоненте.
`import React from 'react';
import { Text, TouchableOpacity, StyleSheet } from 'react-native';
import { COLORS, FONTS } from '../theme/constants';
interface Props {
title: string | JSX.Element;
textColor?: string;
bgColor?: string;
onPress?: () => void;
}
const CustomButton = ({ title, textColor = COLORS.white, bgColor = COLORS.primary, onPress }: Props) => {
return (
<TouchableOpacity
style = {{
...styles.buttonLogin,
backgroundColor: bgColor,
}}
onPress = {onPress}
activeOpacity = {0.8}
>
<Text style = {{
...styles.textButtonLogin,
color: textColor,
...FONTS.body3,
}}>
{title}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
buttonLogin: {
backgroundColor: COLORS.primary,
padding: 15,
width: '90%',
borderRadius: 20,
marginTop: 30,
alignItems: 'center',
margin: 10,
},
textButtonLogin: {
fontSize: 15,
fontWeight: 'bold',
color: COLORS.white,
},
});
export default CustomButton;`
Я не хочу негативно относиться к новым участникам, потому что ценю ваши усилия, но это не ответ на вопрос.
Вам придется включить в оба интерфейса общее свойство, значение которого в каждом из них разное. Узнайте больше о Дискриминационных союзах
interface ButtonProps {
onClick?: (e: Event) => void;
}
interface IconButtonProps extends ButtonProps {
type: 'icon-button';
icon: React.ReactElement;
}
interface TextButtonProps extends ButtonProps {
type: 'text-button';
text: string;
}
function Button(props: IconButtonProps | TextButtonProps) {
return null
}
export default function App() {
return (
<div>
<Button type = "icon-button" icon = {<div />} />
<Button type = "text-button" text = "text" />
</div>
);
}
Это работает, но необходимость добавления дополнительной опоры сводит на нет преимущество. С тем же успехом можно было бы сделать отдельный компонент под названием IconButton
Вы почти закончили, чтобы потребовать одно, а не оба, только отметьте свойство never как необязательное:
interface TextButtonProps extends TypedButtonProps {
text: string
icon?: never
}
interface IconButtonProps extends TypedButtonProps {
icon: JSX.Element
text?: never
}
Если TS когда-либо добавит поддержку точных типов объектов, таких как Flow, то, возможно, вы сможете получить нужные ошибки без дополнительных реквизитов never. Но запрос фичи на точные типы давно открыт...
Другим вариантом было бы создание отдельных компонентов, делегирующих базовый Button:
export const IconButton = (props: IconButtonProps) => <Button {...props} />
export const TextButton = (props: TextButtonProps) => <Button {...props} />
Тогда проверка избыточного свойства TS будет работать - в большинстве случаев. Важно иметь в виду, что избыточная проверка свойств не идеальна, она не гарантирует надежность, как это сделали бы точные типы объектов.
Размеченное объединение , вероятно, вводит в заблуждение: такое размеченное объединение основано на дискриминантном свойстве, которое принимает уникальное литеральное значение, по одному для каждого типа в объединении, что делает явным, какой тип вы обрабатываете, как показано на vighnesh153. ответ.
Но здесь вы не хотите такого дискриминационного свойства.
Почему это работает?
Никакой тип не может быть назначен никогда, поэтому попытка определить свойство/ключ с любым типом (кроме never или undefined) является ошибкой.
Это эффективно запрещает передачу определенного реквизита/ключа с таким именем.
поскольку свойства являются необязательными, я могу избежать определения ни значка, ни текстовой опоры.
Как поясняется в ответе Энди , необязательными должны быть только свойства запрещенного never типа. Вы можете сохранить другие необходимые свойства.
насколько это многословно - если я добавлю больше типов кнопок, мне придется добавить эти новые свойства в каждый отдельный интерфейс.
Мы можем создать вспомогательный тип для факторизации кода. Я думаю, это то, на что ссылается LindaPaiste в своем комментарии к вопросу, и я позволю им описать свое собственное решение, так как это звучит интересно, общий помощник!
В то же время, и конкретно для вашего случая, вы можете сначала создать тип, который запрещает все определенные реквизиты, чтобы вы могли использовать его для каждого типа кнопки, опуская определенные реквизиты, чтобы они стали законными:
// List all specific props to be forbidden
type ButtonSpecificProps = 'text' | 'icon' | 'other' | 'otherCombined' | 'otherOptional'
// Mapped type to convert the list (union of literals) into forbidden keys of an object
type NeverButtonSpecificProps = {
//^? { text?: undefined; icon?: undefined; other?: undefined; otherCombined?: undefined; otherOptional?: undefined }
[P in ButtonSpecificProps]?: never
}
type OnlyButtonProps<T> = T // Legal specific props
& Omit<NeverButtonSpecificProps, keyof T> // Forbidden props, except for legal ones
& TypedButtonProps // Other common props
type TextButtonProps = OnlyButtonProps<{
text: string
}>
type IconButtonProps = OnlyButtonProps<{
icon: JSX.Element
}>
// You can have several legal props simultaneously, possibly some optional
type OtherButtonProps = OnlyButtonProps<{
other: string
otherCombined: boolean
otherOptional?: number
}>
export const Button = ({ onClick, ...props }: IconButtonProps | TextButtonProps | OtherButtonProps) => {
//...
return null
}
() => (
<>
{/* Types of property 'text' are incompatible.
Type 'string' is not assignable to type 'undefined'. */}
<Button icon = {<div />} text='test' uiVariant='default' />
{/* Type '{}' is not assignable to type 'IntrinsicAttributes & (TextButtonProps | IconButtonProps | OtherButtonProps)'. */}
<Button />
<Button text='foo' />
<Button icon = {<div />} />
{/* Property 'otherCombined' is missing in type '{ other: string; }' but required in type '{ other: string; otherCombined: boolean; otherOptional?: number | undefined; }'. */}
<Button other='foo' />
<Button other='foo' otherCombined = {true} />
<Button other='foo' otherCombined = {true} otherOptional = {2} />
</>
)
Ссылка на игровую площадку
Я ценю детали, которые вы подробно описали, это многое объясняет. Я могу вернуться к этому подходу для более сложных случаев, но на сегодня вопрос Энди решает его для меня.
У меня есть тип утилиты Either, который я использую в таких случаях. Сейчас на моем телефоне, но я могу ответить позже.