Home > OS >  How to set up this union in typescript
How to set up this union in typescript

Time:06-25

I have a function that returns one of a handful of possible JSX elements. Each element has their own defined typescript props.

const Thing1 = (props) => <div>Thing 1</div> 
const Thing2 = (props) => <div>Thing 2</div> 

type Thing1Props = {
  one: string;
  two: string;
}

type Thing2Props = {
  three: string;
  four: string;
}

const elementList: { name: string; elm: (props: ElementList) => JSX.Element }[] = [
  { name: 'thing1', elm: (props: Thing1Props) => <Thing1 {...props} /> },
  { name: 'thing2', elm: (props: Thing2Props) => <Thing2 {...props} /> },
];

export type ElementList = Thing1Props | Thing2Props;

I'm getting an error from typescript that reads:

TS2322: Type '(props: { one: string; two: string;}) => JSX.Element' is not assignable to type '(props: ElementList) => Element'.   
Types of parameters 'props' and 'props' are incompatible.     
Type 'ElementList' is not assignable to type '{ one: string; two: string; }'.       
Type '{ three: string; four: string; }' is missing the following properties from type '{ one: string; two: string; }': one, two

Typescript seems to be viewing ElementList as an AND instead of an OR. Not sure what to change here. I'm guessing because it doesn't know which element I'm calling upon it can't define the types.

Here's the issue in typescript playground

CodePudding user response:

The problem with the type { name: string; elm: (props: ElementList) => JSX.Element } is elm needs to be a function that accepts any possible argument of type ElementList. Assuming you have the --strictFunctionTypes compiler option enabled (and you should), then TypeScript will not allow a function of this type to choose to only accept some arguments of type ElementList. Functions may not safely narrow their input types. But they can widen them; a function of type (props: ElementList | string) => JSX.Element would be acceptable there, since if it accepts ElementList | string then it definitely accepts ElementList. It is said that functions are contravariant in their argument types; a specific instance of a function type can widen but not narrow its argument types. So this isn't the type you want.

Instead it seems that you want to represent the correlation between the name property and the type of the props parameter of elm. So the actual type you want looks like:

type Elem = {
    name: "thing1";
    elm: (props: Thing1Props) => JSX.Element;
} | {
    name: "thing2";
    elm: (props: Thing2Props) => JSX.Element;
};

const elementList: Elem[] = [
  { name: 'thing1', elm: (props: Thing1Props) => <Thing1 {...props} /> },
  { name: 'thing2', elm: (props: Thing2Props) => <Thing2 {...props} /> },
]; // okay

That is, instead of having props be a union type, you want the entire object to be a union type. In fact, this Elem type is a discriminated union, where you can check the name property to see which props it needs:

function processElem(elem: Elem) {
  switch (elem.name) {
    case "thing1": return elem.elm({ one: "", two: "" });
    case "thing2": return elem.elm({ three: "", four: "" });
  }
}

You could write that Elem type out by hand, but this could be tedious if you have a lot of Things. Instead you could start with a mapping interface like

interface ThingPropMap {
  thing1: Thing1Props,
  thing2: Thing2Props
}

to represent the correlation, and then have the compiler evaluate Elem like

type Elem = { [K in keyof ThingPropMap]:
  { name: K, elm: (props: ThingPropMap[K]) => JSX.Element }
}[keyof ThingPropMap]

, which is a distributive object type as coined in microsoft/TypeScript#47109. A distributive object type is of the form {[K in X]: F<K>}[X] where X is some keylike type or union of keylike types, and where F<K> is some type function that operates on a keylike type. By immediately indexing into a mapped type, you end up getting the union of F<K> for every K in X. So if X is K1 | K2 | K3, then the distributive object type is F<K1> | F<K2> | F<K3>.

So the Elem definition then computes the union of { name: K, elm: (props: ThingPropMap[K]) => JSX.Element } for every K in keyof ThingPropMap.

Again, this is not necessary if you're willing to write out the discriminated union by hand.

Playground link to code

  • Related