Crocsx
Crocsx

Reputation: 7620

Assigned a type based on a extended type is not processed correctly

EASY explanation at the end :

I have the following case, a type looking like this :

type ItemType<Advanced> = Advanced extends true
  ? BrandsListQuery['brands']['items'][0]
  : MinimalBrandsListQuery['brands']['items'][0];

Advanced is passed to a function like this

const BrandSelectGn = <
  Advanced extends boolean = false
>() = {}

Now the issue I have is in the code I am trying to map a return type to one or the other type,

Saying BrandsListQuery['brands']['items'][0] and MinimalBrandsListQuery['brands']['items'][0] have basically the same format, just the items are different (I do this to have type completion in the useLoadOptions method

interface RequiredResponse<I> {
  [key: string]: {
    total: number;
    items: readonly I[];
  };
}


export const useLoadOptions = <
  I,
  LQ extends RequiredResponse<I>,
>({})


  const { loadOptions } = useLoadOptions<
    ItemType<Advanced>,
    Advanced extends true ? BrandsListQuery : MinimalBrandsListQuery,
  >({

but typescript do not recognized that essentially what I am saying is its the same structure and based on Advanced it is one or the other

TLDR: Now this might be confusing, so I put a simplified playground : Typescript Playground

interface InterA {
  test: {
    total: number;
    items: Array<{itemTypeA: string}>;
  };
}

interface InterB {
  test: {
    total: number;
    items: Array<{itemTypeB: string}>;
  };
}

type ItemType<Advanced> = Advanced extends true ? InterA['test']['items'][0] : InterB['test']['items'][0];

interface RequiredResponse<I> {
  test: {
    total: number;
    items: I[];
  };
}

export const useLoadOptions = <
  I,
  LQ extends RequiredResponse<I>,
>() => {}


const demoFunc = <Advanced extends boolean = boolean>() => {
    useLoadOptions<
    ItemType<Advanced>,
    Advanced extends true ? InterA : InterB, // this line should be correct, if Advanced extends true, then ItemType<Advanced> is InterA else it s interB
  >()
}

Upvotes: 0

Views: 60

Answers (1)

hackape
hackape

Reputation: 19987

TL;DR: ItemType should be:

type ItemType<Advanced> = 
  (Advanced extends true ? InterA : InterB)['test']['items'][0];

Typescript Playground

I don't have authoritative explanation for this, and probably it's just implementation defined behavior that isn't documented anywhere. I never really understand how generic type argument is further interpreted within function body.

But my personal mind model about this mess, is to view it as Schrodinger's cat.

Conditional type over an undetermined generic type argument, is like an unobserved superposition of both-dead-and-alive state. Intellisense tooltip will just show sth like Advanced extends true ? InterA : InterB as a whole, without further interpretation.

Accessing this superposition entity by adding (...)["some_key"] behind it, is like you observe it, and it'll collapse into either-dead-or-alive state. Intellisense tooltip will try its best effort to interpret the conditional type as distributive over union somehow, and show sth like A | B.

I believe this behavior is partly addressed in the handbook:

When conditional types act on a generic type, they become distributive when given a union type.

But it doesn't concretely state "when" exactly will it "act". It seems to me accessing the thing with a key triggers the action.

For comparison, check this out. We keep ItemType2 the way it was.

  • When Advanced is an undetermined generic type argument, even with extends boolean constraint, it'll still error.
  • But when it's a concrete union type Advanced = boolean; // equiv. to "true | false" union, the distributive-over-union behavior is triggered, error gone, point proved.
type ItemType2<Advanced> = Advanced extends true ? InterA['test']['items'][0] : InterB['test']['items'][0];

const demoFunc2 = <Advanced extends boolean = boolean>() => {
    useLoadOptions<
    ItemType2<Advanced>,
    Advanced extends true ? InterA : InterB // error! ❌
  >()
}

const demoFunc3 = () => {
    type Advanced = boolean;
    useLoadOptions<
    ItemType2<Advanced>,
    Advanced extends true ? InterA : InterB // ok ✅
  >()
}

Upvotes: 1

Related Questions