I am trying to enforce certain properties of a component depending on the type of the component... Basically I have a main interface with heroStyling types:
export type HeroStyling = 'SimpleForm' | 'WithTickerHeader' | 'WithTagLineHeader' | 'WithRating'
export interface AdaptiveHeroMainProps {
heroStyling: HeroStyling
cta: string
ctaUrl: string
image: FluidObject
imageAlt: string
}
and I am extending this to four other classes depending on the type of the heroStyling key:
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
}
My question is how can I enforce an extended class properties depending on what the type heroStyling is on my component without TypeScript complaining about one property not existing on a type? I need to deconstruct all props as I render according to their truthy or falsey values.
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>
)
}
Question after captain-yossarian great help: If I render the component as below I get no errors but I should since bulletPoints is not part of the WithRating interface. TS complains only when one of the defined interface properties is missing, not when there is one that shouldn't exist.
<AdaptiveHero
heroStyling={'WithRating'}
reviewStars={true}
textCenter={true}
cta={hero.cta}
ctaUrl={hero.ctaUrl}
image={hero.image.fluid}
imageAlt={hero.image.alt}
bulletPoints={hero.bulletPoints}
/>
CodePudding user response:
If you have a union, you are allowed to use only common properties which are shared across each union type.
You are not allowed to use headingTagLine
without narrowing.
Please see the 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.
In order to narrow the union type, you should create custom typeguard, which has impact on runtime.
However, it is possible to handle it without affection runtime. COnsider this example:
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 https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
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 >
)
}
Please read this question/answer in order to understand whats going on.
UPDATE
You should be aware of that your union is not discriminated. In order to do that, you should add unique property to each type:
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
}
Full code:
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
/>