I have a react component that has 3 variants. I have my props typed like this:
enum VariantType {
VARIANT_1 = "variant_1",
VARIANT_2 = "variant_2",
VARIANT_3 = "variant_3",
}
type BaseProps = {
a: string;
}
type Variant1Props = BaseProps & {
variant: VariantType.VARIANT_1;
b: never;
}
type Variant2Props = BaseProps & {
variant: VariantType.VARIANT_2;
b: number;
}
type Variant3Props = BaseProps & {
variant: VariantType.VARIANT_3;
b: boolean;
}
export const FancyComponent: FunctionComponent<Variant1Props | Variant2Props | Variant3Props> = (props) => {
const someNumber = props.variant === VariantType.VARIANT_2 ? props.b : 123;
...
}
In the assignment of someNumber TS knows that b
is defined because I checked the variant. When using the component TS complains when b
is present when it's VARIANT_1
and it complains when b
is not present/not a number when it's VARIANT_2
(same for VARIANT_3
).
What I'm looking for now is a way to make the variant
option optional when using the component and it should default to VARIANT_1
and you could set it to VARIANT_2
or VARIANT_3
without loosing any of the type-safety I have now.
I tried stuff with generics and so on but couldn't get it to work.
I'm using Typescript 4.5.
Just make the variant
property in Variant1Props
optional, and it all works:
enum VariantType {
VARIANT_1 = "variant_1",
VARIANT_2 = "variant_2",
VARIANT_3 = "variant_3",
}
type BaseProps = {
a: string;
}
type Variant1Props = BaseProps & {
variant?: VariantType.VARIANT_1;
b: never;
}
type Variant2Props = BaseProps & {
variant: VariantType.VARIANT_2;
b: number;
}
type Variant3Props = BaseProps & {
variant: VariantType.VARIANT_3;
b: boolean;
}
// to check that props.b actually has type never
declare function expectTheUnexpected(wtf: never): void;
export const FancyComponent = (props: Variant1Props | Variant2Props | Variant3Props) => {
const someNumber = props.variant === VariantType.VARIANT_2 ? props.b : 123;
if (!props.variant) expectTheUnexpected(props.b);
// ...
}
And in the Playground.
But realistically, you don’t want b: never
—that is telling TS to expect (and require) an actual property b
, but that the value of that property is never
. That means that { a: 'foo' }
is not a valid Variant1Props
, so <FancyComponent a="foo" />
won’t work. Instead, a valid Variant1Props
needs to be something like { a: 'foo', b: someVariableWithTypeNever }
,¹ which is a mess. Really, never
is rarely a type you actually expect to work with, it’s supposed to indicate that something went wrong and we’re now in a situation that the type system says can’t happen.
What you actually want for the definition of Variant1Props
is
type Variant1Props = BaseProps & {
variant?: VariantType.VARIANT_1;
b?: undefined;
}
Now b
may or may not actually be present, but if you include it, it’s gotta be just undefined
(which means it may as well not be present). So { a: 'foo' }
works, but { a: 'foo', b: 42 }
doesn’t—42
isn’t undefined
. To be allowed to put b: 42
, we need to put in variant: VariantType.VARIANT_2
.
<FancyComponent a="foo" b={someVariableWithTypeNever} />
—I’m going to stop bringing React into this, it doesn’t affect the question at all.