I want to create a React alert component like <alert id="component" />
and there is also another prop that I want to choose from one of "info", "success", "warning", "error"
, so as to display different message color, the component is like <alert id="component" info/>
or <alert id="component" success/>
With Typescript, how can I make the interface has only one value from the given four options?
What I did now is:
type MessageStringType = "info" | "success" | "warning" | "error";
type MessageTypes = {
[K in MessageStringType]: boolean;
};
type oneMessageTypes = Partial<MessageTypes>;
interface Props extends oneMessageTypes {
id: string;
}
but this one allowed empty value from "info", "success", "warning", "error"
, how can I make the type check required at least one of the value?
It's like:
<Component id="abc" info /> //correct
<Component id="abc" success /> //correct
<Component id="abc" /> //wrong
<Component id="abc" info success /> //wrong
CodePudding user response:
There's two pieces here.
First, you need to figure out the type that will for this if you hard coded it. And another way to say "must have only one" is "one must have a value, all others must be missing or undefined".
type Test =
| { a: string, b?: void }
| { a?: void, b: string }
const a: Test = { a: 'asd' }
const b: Test = { b: 'asd' }
const c: Test = {} // error
const d: Test = { a: 'asd', b: 'asd' } // error
This creates a union where all keys are known to the union, but each member of the union only allows one of those keys to have a value.
This will be the basis for your props type.
The second half is generating that type from the union of strings. This is trickier.
You need to distribute that union into a new type. One where you get a new member for each member of the source union. And there's a weird trick for that, which is to use a conditional type. (See this answer).
Using that We can make a type like this:
type UnionToBooleanProps<T extends string> =
T extends string ?
{ [otherKeys in Exclude<MessageStringType, T>]?: void } &
{ [key in T]: boolean }
: never ;
Let's go through this.
type UnionToBooleanProps<T extends string> =
This type accepts a generic type parameter T
for the union of strings that name the required property for each member.
T extends string ?
This starts a conditional type that is required in order to make sure we end up with a union and not an object type that has a union value.
{ [otherKeys in Exclude<MessageStringType, T>]?: void } &
This declares all the keys that must NOT have a value. It maps over all keys one at a time, and for each key that doesn't match the one for this union member (this is what Exclude
does), we declare that it cannot have a value.
{ [key in T]: boolean }
Which is then intersected with this, which adds back in the the one key that was not excluded by the previous line and declares that it must have a value of boolean
.
: never
This last line is the falsy branch of the conditional type that will not ever be used. We just need it to make the conditional type syntactically valid.
And with that, everything works!
import React from 'react'
type UnionToBooleanProps<T extends string> =
T extends string ?
{ [otherKeys in Exclude<MessageStringType, T>]?: void } &
{ [key in T]: boolean }
: never ;
type MessageStringType = "info" | "success" | "warning" | "error";
type OneMessageTypes = UnionToBooleanProps<MessageStringType>
type Props = OneMessageTypes & {
id: string;
}
function Component(props: Props) {
return <></>
}
const a = <Component id="abc" info /> //correct
const b = <Component id="abc" success /> //correct
const c = <Component id="abc" /> //wrong
const d = <Component id="abc" info success /> //wrong
All that said, @Nishant is probably right with their comment. Just having a { type: MessageStringType }
will probably be a better API for the consumers of this component.