bordeltabernacle
bordeltabernacle

Reputation: 1653

Union type props in TypeScript & React

I have a component with a prop that I want to restrict to a selection of strings, so am using a union type, like so:

type HeadingProps = {
    level?: 'h1' | 'h2' | 'h3' | 'h4'| 'h5' | 'h6'
}

const Heading: React.FC<HeadingProps> = ({children, level = 'h2'}) => {
    return <Box as={level} />
}

This works fine when I use it as so..

<Heading level="h1">Hello, world!</Heading>

But if I try and use it with an array I get an error:

{['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].map((level: string) => (
    <Heading level={level}>{level}</Heading>
)}

Type 'string' is not assignable to type '"h1" | "h2" | "h3" | "h4" | "h5" | "h6" | undefined'.ts(2322)

How can I have the type of the prop so that only these strings are valid, but enable a valid string value be passed when the component is used in an array like above?

EDIT This is how the types are currently:

export const HeadingLevels = {
  h1: `h1`,
  h2: `h2`,
  h3: `h3`,
  h4: `h4`,
  h5: `h5`,
  h6: `h6`,
}
export type Level = keyof typeof HeadingLevels
type HeadingProps = BoxProps & {
  level?: Level
}

Upvotes: 1

Views: 1381

Answers (2)

Alexey Kureev
Alexey Kureev

Reputation: 2057

If you split these types, you can use the union type (level) to annotate level argument of the mapping function as shown below:

type Level = 'h1' | 'h2' | 'h3' | 'h4'| 'h5' | 'h6'
type HeadingProps = {
    level?: Level
}

const Heading: React.FC<HeadingProps> = ({children, level = 'h2'}) => {
    return <Box as={level} />
}

{['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].map((level: Level) => (
    <Heading level={level}>{level}</Heading>
))}

Upvotes: 0

jcalz
jcalz

Reputation: 327724

['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] is inferred by the compiler as type string[] because often that's the right thing to do, as in

const arr = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; // inferred as string[]
arr.push("h7"); // okay

But in your case you want the compiler to know that the contents of that array are and will always be (for as long as you use it, anyway), the particular string literals you set.

As of TypeScript 3.4, the easiest way to deal with this is to use a const assertion to tell the compiler that your array is intended to be something from which you are reading particular string literals:

(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const).map((level) => (
    <Heading level={level}>{level}</Heading>
))

This should work without error. Note that I removed the type annotation from level. You don't want to widen level to string, since then the compiler can't be sure anymore that it's appropriate. Instead, let the compiler infer the type of level, which it will do as your union "h1"|"h2"|...|"h6", as desired.

Okay, hope that helps; good luck!

Link to code

Upvotes: 5

Related Questions