Ровно один/размеченный союз для реактивного реквизита

Я пытаюсь определить тип машинописного текста для реквизита компонента реакции. Мой компонент — это базовая кнопка, которая принимает либо опору 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.

Почему это работает? Я не в восторге от того, насколько это многословно — если я добавлю больше типов кнопок, мне придется добавить эти новые свойства в каждый отдельный интерфейс.

Моя вторая проблема заключается в том, что, поскольку свойства являются необязательными, я могу избежать определения ни значка, ни текстовой поддержки - помните, что мне нужно убедиться, что одно или другое существует.

Есть ли более чистое решение, которое удовлетворило бы мои потребности?

У меня есть тип утилиты Either, который я использую в таких случаях. Сейчас на моем телефоне, но я могу ответить позже.

Linda Paiste 11.01.2023 03:22

Может что-то вроде этого?

ghybs 11.01.2023 03:25

FWIW, это не совсем конкретный вопрос React, у вас может возникнуть проблема с вызовом любой функции, которая принимает тип объединения.

Andy 11.01.2023 03:41
От React к React Native: Руководство для начинающих по разработке мобильных приложений с использованием React
От React к React Native: Руководство для начинающих по разработке мобильных приложений с использованием React
Если вы уже умеете работать с React, создание мобильных приложений для iOS и Android - это новое приключение, в котором вы сможете применить свои...
Революционная веб-разработка ServiceNow
Революционная веб-разработка ServiceNow
В быстро развивающемся мире веб-разработки ServiceNow для достижения успеха крайне важно оставаться на вершине последних тенденций и технологий. По...
Как добавить SEO(Search Engine Optimization) в наше веб-приложение и как это работает?
Как добавить SEO(Search Engine Optimization) в наше веб-приложение и как это работает?
Заголовок веб-страницы играет наиболее важную роль в SEO, он помогает поисковой системе понять, о чем ваш сайт.
Руководство для начинающих по веб-разработке на React.js
Руководство для начинающих по веб-разработке на React.js
Веб-разработка - это захватывающая и постоянно меняющаяся область, которая постоянно развивается благодаря новым технологиям и тенденциям. Одним из...
Разница между Angular и React
Разница между Angular и React
React и AngularJS - это два самых популярных фреймворка для веб-разработки. Оба фреймворка имеют свои уникальные особенности и преимущества, которые...
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
2
3
69
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Вам не нужно определять два интерфейса, чтобы установить значения, которые получит ваш компонент, достаточно одного интерфейса. Укажите, какие параметры вам нужны. в случае значка, который является названием строки типа, также заголовок кнопки типа строка; затем объявите значения, которые не потребуются, и инициализируйте их в своем компоненте.

`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;`

Я не хочу негативно относиться к новым участникам, потому что ценю ваши усилия, но это не ответ на вопрос.

Linda Paiste 11.01.2023 03:20

Вам придется включить в оба интерфейса общее свойство, значение которого в каждом из них разное. Узнайте больше о Дискриминационных союзах

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

helion3 11.01.2023 03:27
Ответ принят как подходящий

Вы почти закончили, чтобы потребовать одно, а не оба, только отметьте свойство 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} />
    </>
)

Ссылка на игровую площадку

Я ценю детали, которые вы подробно описали, это многое объясняет. Я могу вернуться к этому подходу для более сложных случаев, но на сегодня вопрос Энди решает его для меня.

helion3 11.01.2023 07:19

Другие вопросы по теме