我如何根据 React 中的类型条件强制执行不同的打字稿接口 类?

How can I enforce different typescript interface classes based on a type condition in React?

我正在尝试根据组件的类型强制执行组件的某些属性...基本上我有一个带有 heroStyling 类型的主界面:

export type HeroStyling = 'SimpleForm' | 'WithTickerHeader' | 'WithTagLineHeader' | 'WithRating'

export interface AdaptiveHeroMainProps {
  heroStyling: HeroStyling
  cta: string
  ctaUrl: string
  image: FluidObject
  imageAlt: string
}

我正在将其扩展到其他四个 classes,具体取决于 heroStyling 键的类型:

export interface AdaptiveHeroMainProps {
  heroStyling: HeroStyling
  cta: string
  ctaUrl: string
  image: FluidObject
  imageAlt: string
}

export interface WithTagLineHeader extends AdaptiveHeroMainProps {
  headingTagLine: string
  heading: string
  leadParagraph: string
  bulletPoints?: string
}
export interface WithTickerHeader extends AdaptiveHeroMainProps {
  textCenter: boolean
  leadParagraph?: string
  bulletPoints?: string
}

export interface SimpleForm extends AdaptiveHeroMainProps {
  textCenter: boolean
  heading: string
  leadParagraph?: string
  bulletPoints?: string
}
export interface WithRating extends AdaptiveHeroMainProps {
  textCenter: true
  reviewStars?: boolean
}

我的问题是如何根据我的组件上的 heroStyling 类型强制执行扩展的 class 属性,而 TypeScript 不会抱怨类型上不存在 属性?我需要在渲染时根据真实值或虚假值解构所有道具。

 const CampaignHero = ({ 
    heroStyling, textCenter, reviewStars, headingTagLine, 
    heading, leadParagraph, bulletPoints, cta, ctaUrl, image, imageAlt }
    : WithTagLineHeader | WithTickerHeader | SimpleForm | WithRating ): React.ReactElement => {
return (
    <section>
          {headingTagLine && (
            <div      
              dangerouslySetInnerHTML={{ __html: headingTagLine }}
            />
          )}
          {heading && <h1>{heading}</h1>}
          {leadParagraph && <p>{leadParagraph}</p>}
          {bulletPoints && (
            <div
              dangerouslySetInnerHTML={{ __html: bulletPoints }}
            />
          )}
            <Button href={ctaUrl}>{cta}</Button>
          {reviewStars && (
              <HeroRating />
          )}
        </div>
          <Img alt={imageAlt} fluid={image} loading="eager" />
    </section>
  )
}

captain-yossarian大帮忙后的提问: 如果我如下所示渲染组件,我不会收到任何错误,但我应该这样做,因为 bulletPoints 不是 WithRating 接口的一部分。 TS 仅在缺少定义的接口属性之一时才抱怨,而不是在存在不应存在的属性时抱怨。

<AdaptiveHero
        heroStyling={'WithRating'}
        reviewStars={true}
        textCenter={true}
        cta={hero.cta}
        ctaUrl={hero.ctaUrl}
        image={hero.image.fluid}
        imageAlt={hero.image.alt}
        bulletPoints={hero.bulletPoints}
      />

如果您有一个联合体,您只能使用在每个联合体类型之间共享的公共属性。 不缩小就不允许使用 headingTagLine。 请参阅 docs:

Sometimes you’ll have a union where all the members have something in common. For example, both arrays and strings have a slice method. If every member in a union has a property in common, you can use that property without narrowing: This is the only way to preserve type safety.

为了缩小联合类型,你应该创建custom typeguard,这对运行时有影响。

但是,可以在不影响运行时的情况下处理它。考虑这个例子:

import React, { FC } from 'react'

type FluidObject = {
  tag: 'FluidObject'
}

export type HeroStyling = 'SimpleForm' | 'WithTickerHeader' | 'WithTagLineHeader' | 'WithRating'

export interface AdaptiveHeroMainProps {
  heroStyling: HeroStyling
  cta: string
  ctaUrl: string
  image: FluidObject
  imageAlt: string
}

export interface AdaptiveHeroMainProps {
  heroStyling: HeroStyling
  cta: string
  ctaUrl: string
  image: FluidObject
  imageAlt: string
}

export interface WithTagLineHeader extends AdaptiveHeroMainProps {
  headingTagLine: string
  heading: string
  leadParagraph: string
  bulletPoints?: string
}
export interface WithTickerHeader extends AdaptiveHeroMainProps {
  textCenter: boolean
  leadParagraph?: string
  bulletPoints?: string
}

export interface SimpleForm extends AdaptiveHeroMainProps {
  textCenter: boolean
  heading: string
  leadParagraph?: string
  bulletPoints?: string
}


type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
  T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

// credits goes to 
type StrictUnion<T> = StrictUnionHelper<T, T>

const Button: FC<{ href: string }> = ({ children }) => <span> children</span>

const CampaignHero = ({
  heroStyling, textCenter, reviewStars, headingTagLine,
  heading, leadParagraph, bulletPoints, cta, ctaUrl, image, imageAlt }
  : StrictUnion<WithTagLineHeader | WithTickerHeader | SimpleForm>): React.ReactElement => {
  return (
    <section>
      {headingTagLine && (
        <div
          dangerouslySetInnerHTML={{ __html: headingTagLine }}
        />
      )}
      {heading && <h1>{heading}</h1>}
      {leadParagraph && <p>{leadParagraph}</p>}
      {bulletPoints && (
        <div
          dangerouslySetInnerHTML={{ __html: bulletPoints }}
        />
      )}
      <Button href={ctaUrl}>{cta}</Button>
      {reviewStars && (
        <HeroRating />
      )}
      <Img alt={imageAlt} fluid={image} loading="eager" />
    </section >
  )
}

Playground

请阅读此 以了解发生了什么。

更新

您应该知道您的工会没有受到歧视。 为此,您应该为每种类型添加独特的 属性:

export interface AdaptiveHeroMainProps {
  heroStyling: HeroStyling
  cta: string
  ctaUrl: string
  image: FluidObject
  imageAlt: string
}

export interface WithTagLineHeader extends AdaptiveHeroMainProps {
  heroStyling: 'WithTagLineHeader' // unique
  headingTagLine: string
  heading: string
  leadParagraph: string
  bulletPoints?: string
}
export interface WithTickerHeader extends AdaptiveHeroMainProps {
  heroStyling: 'WithTickerHeader' // unique
  textCenter: boolean
  leadParagraph?: string
  bulletPoints?: string
}

export interface SimpleForm extends AdaptiveHeroMainProps {
  heroStyling: 'SimpleForm' // unique
  textCenter: boolean
  heading: string
  leadParagraph?: string
  bulletPoints?: string
}
export interface WithRating extends AdaptiveHeroMainProps {
  heroStyling: 'WithRating' // unique
  textCenter: true
  reviewStars?: boolean
}

完整代码:

import React from 'react'
export type HeroStyling = 'SimpleForm' | 'WithTickerHeader' | 'WithTagLineHeader' | 'WithRating'

type FluidObject = {
  tag: 'FluidObject'
}
export interface AdaptiveHeroMainProps {
  heroStyling: HeroStyling
  cta: string
  ctaUrl: string
  image: FluidObject
  imageAlt: string
}

export interface WithTagLineHeader extends AdaptiveHeroMainProps {
  heroStyling: 'WithTagLineHeader'
  headingTagLine: string
  heading: string
  leadParagraph: string
  bulletPoints?: string
}
export interface WithTickerHeader extends AdaptiveHeroMainProps {
  heroStyling: 'WithTickerHeader'
  textCenter: boolean
  leadParagraph?: string
  bulletPoints?: string
}

export interface SimpleForm extends AdaptiveHeroMainProps {
  heroStyling: 'SimpleForm'
  textCenter: boolean
  heading: string
  leadParagraph?: string
  bulletPoints?: string
}
export interface WithRating extends AdaptiveHeroMainProps {
  heroStyling: 'WithRating'
  textCenter: true
  reviewStars?: boolean
}

const CampaignHero = (props: WithTagLineHeader | WithTickerHeader | SimpleForm | WithRating): React.ReactElement => {
  if(props.heroStyling==='WithRating'){
    props.textCenter  // true
  }
  if(props.heroStyling==='WithTickerHeader'){
    props.textCenter // boolean
  }
  // etc ...
  return (
    <section>

    </section>
  )
}

<CampaignHero
  heroStyling={'WithRating'}
  reviewStars={true}
  textCenter={true}
  cta={hero.cta}
  ctaUrl={hero.ctaUrl}
  image={hero.image.fluid}
  imageAlt={hero.image.alt}
  bulletPoints={hero.bulletPoints} // expected error
/>

Playground