Description
openedon Oct 23, 2021
Bug Report
I'm not exactly sure what the bug is because it feels rather arbitrary, but I suspect there's some limit to distributive conditional types I'm running into. The repro below (tried to make it as minimal as possible) is the easiest way to demonstrate it.
The problem is I have a conditional type Conditional<T>
where I pass in C = A | B
and B
itself is a union type composed of 26 items. In the conditional type, I branch on T extends A ? AWrapper<A> : T extends B ? BWrapper\<B\> : ....
.
I then make Conditional the return type of a function, and try to return a simplified case of BWrapper<B>
, but it fails to compile. The interesting part though is it only fails when B
is a union type of 26 items and not when it's just 25 items.
Everything also works if I change the conditional type to be non-distributive: [T] extends [A] ? AWrapper<A> : [T] extends [B] ? BWrapper<B> : ...
I'm sure a suggestion might be to make Conditional
a union type of AWrapper<A> | BWrapper<B>
here instead of a conditional type, but I explain why at the bottom I'd prefer to avoid that.
In summary, I'm not sure why when B
is > 25 items it doesn't work.
🔎 Search Terms
25 26 enum distributive conditional type limit
🕗 Version & Regression Information
I see this bug in all versions of TS that are usable in the TS playground
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about distributive conditional types. I actually figured out the workaround through Generic conditional type T extends never ? 'yes' : 'no' resolves to never when T is never. #31751
⏯ Playground Link
Playground link with relevant code
💻 Code
enum FieldWithoutId {
NOID1,
NOID2,
NOID3,
NOID4,
NOID5,
NOID6,
NOID7,
NOID8,
NOID9,
NOID10,
NOID11,
NOID12,
NOID13,
NOID14,
NOID15,
NOID16,
NOID17,
NOID18,
NOID19,
NOID20,
NOID21,
NOID22,
NOID23,
NOID24,
NOID25,
NOID26, // commenting out this line makes everything work
}
enum FieldWithId {
ID1,
ID2
}
type WrapperWithId<T extends FieldWithId = FieldWithId> = {
field: T;
id: number;
};
type WrapperWithoutId<
T extends FieldWithoutId = FieldWithoutId
> = {
field: T;
};
type Field = FieldWithId | FieldWithoutId;
type Wrapper<T extends Field = Field> = T extends FieldWithoutId ? WrapperWithoutId<T>
: T extends FieldWithId ? WrapperWithId<T> : WrapperWithId | WrapperWithoutId;
const toWrapper = (field: Field, id?: number): Wrapper => {
if (field === FieldWithId.ID1 || field === FieldWithId.ID2) {
if (!id) {
throw new Error();
}
return {
field,
id,
};
} else {
const x: FieldWithoutId = field;
// fails to compile. Is distribution of conditional type too large?
return { field: x };
}
}
// Using a "non-distributive" conditional type. Not sure if that's the correct description or what makes this non-distributive though. Wording taken from
type NonDistributiveWrapper<T extends Field = Field> = [T] extends [FieldWithoutId] ? WrapperWithoutId<T>
: [T] extends [FieldWithId] ? WrapperWithId<T> : WrapperWithId | WrapperWithoutId;
const toNonDistributiveWrapper = (field: Field, id?: number): NonDistributiveWrapper => {
if (field === FieldWithId.ID1 || field === FieldWithId.ID2) {
if (!id) {
throw new Error();
}
return {
field,
id,
};
} else {
const x: FieldWithoutId = field;
return { field: x }; // works. Why?
}
}
// Want to be able to do this, which prevents making Wrapper a union type?
type Container<T extends Field = Field> = {
wrapper: Wrapper<T>;
value: T extends FieldWithoutId ? undefined : number;
}
function foo(container: Container<FieldWithId>) {
return container.wrapper.id + container.value;
}
// If we try to make Wrapper a union type instead (which is reasonable), we can't do that:
export type Wrapper2 = WrapperWithId | WrapperWithoutId;
type ContainerNoParameterizedWrapper<T extends Field = Field> = {
wrapper: Wrapper2;
value: T extends FieldWithoutId ? undefined : number;
}
function foo2(container: ContainerNoParameterizedWrapper<FieldWithId>) {
return container.wrapper.id + container.value; // fails to compile
}
🙁 Actual behavior
Error:
Type '{ field: FieldWithoutId; }' is not assignable to type 'WrapperWithId<FieldWithId.ID1> | WrapperWithId<FieldWithId.ID2> | WrapperWithoutId<FieldWithoutId.NOID1> | ... 24 more ... | WrapperWithoutId<...>'.
Type '{ field: FieldWithoutId; }' is not assignable to type 'WrapperWithoutId<FieldWithoutId.NOID26>'.
Types of property 'field' are incompatible.
Type 'FieldWithoutId' is not assignable to type 'FieldWithoutId.NOID26'
🙂 Expected behavior
No error, which is what happens in the non-distributive case or the case where FieldWithoutId
only contains 25 items.