Generate complex union type from another complex union type

Viewed 58

I have this data structure :

type FirstOne = {
  type: 'first-one',
  uniqueKey1111: 1,
};
type FirstTwo = {
  type: 'first-two',
  uniqueKey2222: 2,
}
type First = {
  type: 'first',
  details: FirstOne | FirstTwo
}
type SecondOne = {
  type: 'second-one',
  uniqueKey3: 3,
};
type SecondTwo = {
  type: 'second-two'
};
type Second = {
  type: 'second',
  details: SecondOne | SecondTwo,
}
type DataType = First | Second;

data have type and subtype, if I select first type don't can select second-one subtype,

I have on page two select, first refer to Data.type, second to Data.details.type I need validate select options:

I have next generic for convert type DataType to SelectOption:

type SelectOption<T> = { name: string, value: T };

I don't known how validate additional options what it's value depend of main select options

type SelectOptions = {
  main: SelectOption<DataType['type']>[],
  additional: SelectOption<DataType['details']['type']>[];
}

const options : SelectOptions = {
  main: [{name: 'name', value: 'first'}],
  additional: [{name: '', value: 'second-two'}],
}

in options now set invalid data, but typescript not check what additionalOptions have option what not compatible with main.

Also I think currently SelectOptions structure don't can exec this checking.

I will be grateful for any idea to solve the problem

I realize what options need have next structure

const options  = {
  main: [{name: 'name', value: 'first'}, {name: 'name', value: 'second'}],
  additional: {
    first: [{name: '', value: 'first-one'}, {name: '', value: 'first-two'}],
    second: [{name: '', value: 'second-one'}, {name: '', value: 'second-two'}]
  },
};
1 Answers

You have not said this explicitly, but I presume you want SelectOptions to look something like this:

type SelectOptions = {
    main: SelectOption<"first">[];
    additional: SelectOption<"first-one">[] | SelectOption<"first-two">[];
} | {
    main: SelectOption<"second">[];
    additional: SelectOption<"second-one">[] | SelectOption<"second-two">[];
}

If so, you can indeed get TypeScript to compute SelectOptions in terms of DataType, but you need to do so by distributing your definition across the (nested) unions in DataType.

Skip this next section if you just want the answer and not the explanation:


There are two ways to distribute across unions I know of; one is distributive conditional types, where you write a type of the form

type D<T> = T extends any ? F<T> : never

, and the compiler will automatically break T into its union constituents before calculating; and so D<A | B> will evaluate to F<A> | F<B>. Note that this is not easy to use inline; you cannot write (A | B) extends any ? F<A | B> : never, because A | B is not a type parameter. You can use conditional type inference as in

type DAB = (A | B) extends infer T ? T extends any ? F<T> : never : never

to declare a type parameter T and distribute across it; this works, but is more complicated and harder to explain to those unused to the intricacies of conditional types.

The other way is to use mapped types which you then immediately index into to get a union of its values. This only works if the union has some literal key-like property in it. Let's say the union T has a property named "type" of a literal type; then you can use key remapping like this:

type D<T extends { type: PropertyKey }> = { 
  [U in T as U['type']]: F<U> 
}[T['type']];

and then again, D<A | B> will evaluate to F<A> | F<B>. This is more complicated looking, but is actually less crazy to use inline. Here you can just replace T with A | B:

type DAB = { [U in (A | B) as U['type']]: F<U> }[(A | B)['type']];

In what follows I will use this latter method:


Here's the definition of SelectOptions:

type SelectOptions = { [DT in DataType as DT['type']]: {
    main: SelectOption<DT['type']>[],
    additional: { [DTD in DT['details']as DTD['type']]:
        SelectOption<DTD['type']>[]
    }[DT['details']['type']]
} }[DataType['type']]

You can see that we are distributing the operation over the DataType union, and for each piece DT of that union, we are distributing further over the DT['details'] union.

You can verify that it evaluates to the definition at the very beginning. Let's make sure it works:

const badOptions: SelectOptions = { // error!
// -> ~~~~~~~~~~
// Type '"first"' is not assignable to type '"second"'.
    main: [{ name: 'name', value: 'first' }],
    additional: [{ name: '', value: 'second-two' }],
};

const goodOptions: SelectOptions = {
    main: [{ name: 'name', value: 'second' }],
    additional: [{ name: '', value: 'second-two' }],
}; // okay

Looks good. The incorrect data has now been rejected, and the correct data has been accepted.

Playground link to code


UPDATE: the new structure is significantly different and probably deserves a different question, but I will provide the answer here without much elaboration (it's just key remapping at this point), and hope the scope of the question does not change further:

type SelectOptions = {
    main: SelectOption<DataType['type']>[],
    additional: { [DT in DataType as DT['type']]: SelectOption<DT['details']['type']>[] }
}

Playground link to code

Related