In my React function component, I have a generic setup where the type T
extends ReactText
or boolean
, and the props include a method that returns a value of type T
.
import React, { FC, ReactText } from 'react';
interface Thing {}
interface Props<T extends ReactText | boolean> {
value: T | null;
mapValues(thing: Thing): T;
}
interface MyComponent<T extends ReactText | boolean> extends FC<Props<T>> {}
function MyComponent<T extends ReactText | boolean>(
{ value, mapValues }: Props<T>
) {
// implementation here
return <div />;
}
However, I want to ensure that the method only returns either a ReactText
or a boolean
, not both. It should not be able to return a different type.
import React, { FC, ReactText } from 'react';
interface Thing {
someBool: boolean;
someStr: string;
}
interface Props<T extends ReactText | boolean> {
value:
boolean extends T ? boolean | null :
ReactText extends T ? ReactText | null :
never;
mapValues(thing: Thing):
boolean extends T ? boolean :
ReactText extends T ? ReactText :
never;
things: Thing[];
}
interface MyComponent<T extends ReactText | boolean> extends FC<Props<T>> {}
function MyComponent<T extends ReactText | boolean>(
{ value, mapValues, things }: Props<T>
) {
if (value != null) {
return <div>{value}</div>;
}
const items = things
.map(mapValues)
.map(v => <li key={v.toString() /** assuming v is unique */}>{v}</li>);
return <ul>{items}</ul>;
}
Even though it works, the solution seems convoluted and potentially fragile.
const things: Thing[] = [
{ someStr: 'a', someBool: false },
{ someStr: 'b', someBool: true }
];
const value = null;
const mapValues = (thing: Thing) =>
things.find(t => t.someStr === thing.someStr)?.someStr ??
things.find(t => t.someBool === thing.someBool)?.someBool ??
false;
// This code works but feels clumsy and may break easily
// @ts-expect-error
<MyComponent things={things} value={value} mapValues={mapValues} />;
// This alternative works
// Why do we need `as ReactText` in this scenario?
<MyComponent things={things} value={'abc' as ReactText} mapValues={() => 'abc' as ReactText} />;
Type '(thing: Thing) => string | boolean' is not assignable to type '(thing: Thing) => boolean'. Type 'string | boolean' is not assignable to type 'boolean'. Type 'string' is not assignable to type 'boolean'.
The current solution isn't very elegant and seems error-prone.
Is there a way to implement an exclusive OR (XOR
) type without relying on interface properties? Maybe leveraging Extract<T, U>
?
Try out this code snippet on the TS Playground.