Reputation: 7620
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
Reputation: 19987
TL;DR: ItemType
should be:
type ItemType<Advanced> =
(Advanced extends true ? InterA : InterB)['test']['items'][0];
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.
Advanced
is an undetermined generic type argument, even with extends boolean
constraint, it'll still error.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