Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distributive conditional type limit over 25 items? #46497

Closed
boconnell opened this issue Oct 23, 2021 · 7 comments
Closed

Distributive conditional type limit over 25 items? #46497

boconnell opened this issue Oct 23, 2021 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@boconnell
Copy link

boconnell commented 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

⏯ 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.

@MartinJohns
Copy link
Contributor

The limit of 25 is not a bug, but an intentional limit. Just search for 25 in:title and look at the existing issues.

@fatcerberus
Copy link

Is this actually that limit, though? The error shown is just a normal assignability error; usually when TS hits the recursion limit it actually produces an error to that effect.

@MartinJohns
Copy link
Contributor

@fatcerberus Yes: #43283. It's not a recursion limit.

@boconnell
Copy link
Author

boconnell commented Oct 25, 2021

This seems a little different, in that the union type definition itself still works with greater than 25 items but only fails when used in a distributive conditional type. It also works in a non-distributive conditional type.

Also, if there is indeed a 25-item limit somewhere, could the error message be improved to indicate that? Otherwise, it feels very arbitrary - we wasted a lot of time trying to figure out the problem when this is a known limitation.

Also, there does not seem to be a canonical feature request issue for increasing this limit or making it configurable - they have all been closed. Can we please get a canonical one we could upvote and subscribe to? It would also it make it more discoverable. This feels like it should be added to the "known issues" section of the FAQ as well.

@fatcerberus
Copy link

fatcerberus commented Oct 25, 2021

From what I've been led to understand, the specific limit involved here is about relating { x: T | U | ... } to { x: T } | { x: U } | ..., which is limited to 25 candidates to avoid a combinatorial explosion (which is easy to do with multiple properties). When the limit is hit, TS just assumes the type is not assignable.

@boconnell
Copy link
Author

boconnell commented Oct 25, 2021

Ah, that's consistent with the error message. OK, here's a more minimal repro: TS playground link.

Inline:

// 26 items
const things = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26] as const;
type Thing = typeof things[number];

type Wrapper<T> = { thing: T }
type DistributiveWrapper<T> = T extends Thing ? Wrapper<T> : never;

function foo(wrapper: DistributiveWrapper<Thing>): Wrapper<Thing> {
    // works
    return wrapper;
}

function bar(wrapper: Wrapper<Thing>): DistributiveWrapper<Thing> {
    // doesnt work
    return wrapper;
}

// Note, only 25 items
const things2 = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25] as const;
type Thing2 = typeof things2[number];

function bar2(wrapper: Wrapper<Thing2>): DistributiveWrapper<Thing2> {
    // works
    return wrapper;
}

@andrewbranch andrewbranch added the Duplicate An existing issue was already created label Oct 25, 2021
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants