I have a generic React function component where T extends ReactText | boolean, and its props contain a method that returns 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>
) {
// do stuff
return <div />;
}
However, I want to enforce that the method returns exactly one of ReactText and boolean; it cannot be a function that returns either.
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>;
}
// super simplified verison of what I'm actually doing
const items = things
.map(mapValues)
.map(v => <li key={v.toString() /** assume v is unique */}>{v}</li>);
return <ul>{items}</ul>;
}
While decidedly ugly, it does work (much to my surprise after writing this out in TS Playground).
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;
// succeeds but is ugly and potentially fragile
// @ts-expect-error
<MyComponent things={things} value={value} mapValues={mapValues} />;
// this one works
// I don't know why `as ReactText` is needed when `ReactText` is `string | number`.
<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'.
I can't imagine, though, that it is the best solution. It's verbose and feels fragile.
Is there an XOR type that isn't based on interface properties, as that doesn't work in this case? Perhaps using Extract<T, U>?