I am trying to understand how I can enforce discriminated unions. I have a type definition that technically typescript will accept, but is not quite working as intended for me.
NOTE: I missed mentioning earlier that I need these type definitions for a functional component.
There is an official way to define a discriminated union. However, in an IDE, it will throw the following error:
Property 'startDate' does not exist on type 'MyComponentProps'.ts(2339)
Example that is accepted by typescript, but reflects an error in the IDE
import { FC } from "react";
interface BaseProps {
name: string;
age: number;
}
type IsEligibleProps = {
eligible: true;
startDate: string;
};
type NotEligibleProps = {
eligible: false;
reason: string;
};
type MyComponentProps = BaseProps & (IsEligibleProps | NotEligibleProps);
const MyComponent: FC<MyComponentProps> = ({
name,
age,
eligible = false,
startDate,
reason
}) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Eligible: {eligible ? "true" : "false"}</p>
<p>Response: {eligible ? startDate : reason}</p>
</div>
);
};
export default MyComponent;
I have managed to find an inefficient solution that requires me to re-declare all the props for all the possible variants, but set the ones I do not need to null/never
.
Example that is accepted by typescript, and does not throw any errors
import { FC } from "react";
interface BaseProps {
name: string;
age: number;
}
type EligibleProps =
| {
eligible: true;
startDate: string;
reason?: never;
}
| {
eligible?: false;
reason: string;
startDate?: never;
};
type MyComponentProps = BaseProps & EligibleProps;
const MyComponent: FC<MyComponentProps> = ({
name,
age,
eligible = false,
startDate,
reason
}) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Eligible: {eligible ? "true" : "false"}</p>
<p>Response: {eligible ? startDate : reason}</p>
</div>
);
};
export default MyComponent;
My question is – is there a way to use the original discriminated unions definition and have the Functional Component also read it, and determine the appropriate corresponding props on a de-structured props list?
Link to erroneous code
Link to working but inefficient code
2
Answers
I think your first code achieved the task.
interface BaseProps {
name: string;
age: number;
}
UPDATE:
I want to offer the most flexible option for solving problems. We really need an interface with all properties. I decided to use its value as a
caption
andval
. Thus, we can create an object with the sorting and header settings we need.Interfaces:
Сomponent:
link to codesandbox
Currently you are only allowed to destructure a union type into an object if that object’s properties exist in every member of the union. Normally that’s a reasonable restriction, but when you have a discriminated union type it would lift it. There’s an open feature request for that at microsoft/TypeScript#46318 marked as "Awaiting More Feedback", so if you want to see this happen it wouldn’t hurt to give that issue a 👍 and possibly describe why your use case is compelling. (It probably wouldn’t help much, either, to get a single extra vote… but it wouldn’t hurt.)
Until and unless that’s implemented, you’ll need to work around it. One workaround is to just give up on destructuring the relevant properties and access them the "normal" way:
Here
rest
is just the part of the object corresponding to the union discriminant and its properties. Then instead of referencingeligible
,startDate
, orreason
, you accessrest.eligible
,rest.startDate
, andrest.reason
, which works.Another workaround is to augment the discriminated union so that each member mentions all the properties from all the other members, and explicitly prohibit the "cross-term" properties by making them optional properties of the impossible
never
type. That will make sure wheneligible
istrue
, thenreason
should be known to beundefined
. For want of a better term I’ll call this "exclusifying" the union.Indeed this is the technique you were talking about as being "inefficient". Luckily you are not required to do this manually. Instead you could write a utility type function to automatically exclusify a union. It could look like this:
Essentially it’s a distributive conditional type that splits
T
into its union members (that’s theT extends unknown ? ⋯ : never
part), and for each one, it intersects in an object type with all the keys from any union member, with the type beingnever
for any key not in the current member.Note that "all the keys from any union member" needs to be computed before we split
T
into union members; I do that by introducingK
and giving it a default generic type argument ofAllKeys<T>
which is a separate distributive conditional type.Also intersections can look ugly, so I have collapsed them into single object types using a technique discussed in How can I see the full expanded contract of a Typescript type? .
Let’s test it:
Looks good; that’s the same type as you had (and so you know it works), but now you can add as many union members as you like and it should automatically be maintained.
Playground link to code