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

Homomorphic mapped generic array type sometimes no longer seen as an array #57744

Closed
jcalz opened this issue Mar 12, 2024 · 6 comments Β· Fixed by #57801
Closed

Homomorphic mapped generic array type sometimes no longer seen as an array #57744

jcalz opened this issue Mar 12, 2024 · 6 comments Β· Fixed by #57801
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Recent Regression This is a new regression just found in the last major/minor version of TypeScript.

Comments

@jcalz
Copy link
Contributor

jcalz commented Mar 12, 2024

πŸ”Ž Search Terms

conditional, mapped, array

πŸ•— Version & Regression Information

⏯ Playground Link

Playground link

πŸ’» Code

type MustBeArray<T extends any[]> = T

type Hmm<T extends any[]> = T extends number[] ?
    MustBeArray<{ [I in keyof T]: 1 }> : // <-- error!
    //          ~~~~~~~~~~~~~~~~~~~~~
    // Type '{ [I in keyof T]: 1; }' does not satisfy the constraint 'any[]'.
    // Types of property 'toString' are incompatible.
    // Type 'number' is not assignable to type '() => string'.(2344)
    never;

type X = Hmm<[3, 4, 5]>
//   ^? type X = [1, 1, 1]

πŸ™ Actual behavior

{[I in keyof T]: 1} is not seen as an array type inside of Hmm<T>, causing a compiler error. Something about the conditional type re-constraining T to another array type causes it to fail.

πŸ™‚ Expected behavior

The code should compile without error.

Additional information about the issue

Comes from this Stack Overflow question.

I'm a bit concerned that I can't seem to work around this without //@ts-ignore, since the normal approaches of Extract<T, any[]> don't help (they end up just doing the same conditional type thing again which break things) and T & any[] kind of works but you end up with intersections of arrays which nobody likes.

@jcalz jcalz changed the title Homomorphic mapped generic array type no longer seen as an array Homomorphic mapped generic array type sometimes no longer seen as an array Mar 12, 2024
@Andarist
Copy link
Contributor

Andarist commented Mar 12, 2024

The constraint of T becomes number[] & any[] here (it comes from getBaseConstraint(getSubstitutionIntersection(t))) and that's not exactly something that satisfies isArrayOrTupleType check in getResolvedApparentTypeOfMappedType.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Mar 12, 2024
@ahejlsberg
Copy link
Member

Right, the core issue is that when a homomorphic mapped type is applied to an intersection of array types, the resulting type isn't an array type:

type CheckArray<T extends any[]> = T;

type Mappy<T> = { [K in keyof T]: 1 };

type T0 = CheckArray<Mappy<[10, 20, 30]>>;  // [1, 1, 1]
type T1 = CheckArray<Mappy<string[]>>;  // 1[]
type T2 = CheckArray<Mappy<string[] & number[]>>;  // Error

The reason the error shows up in the OP is that with #57122 (which is in 5.4) we now recognize that the conditional type adds the constraint number[] to T in the true branch, meaning that when T is any[] we get the constraint any[] & number[].

The fix here is probably to distribute the homomorphic mapped type over intersections, i.e. treat Mappy<string[] & number[]> as Mappy<string[]> & Mappy<number[]>, but I need to think on this a bit.

@jcalz
Copy link
Contributor Author

jcalz commented Mar 13, 2024

Yeah, homomorphic mapped types "should" distribute over intersections, but I guess I find it a little surprising that the combination of two array constraints just produces an intersection to begin with, since intersections of array types are so problematic.

@ehoogeveen-medweb
Copy link

I would (perhaps naively) expect that number[] & string[] reduces to never or never[] and number[] & any[] reduces to number[] (or if any[] is weird, then at least with unknown[]). Is that (deliberately) not the case?

@jcalz
Copy link
Contributor Author

jcalz commented Mar 14, 2024

Could someone triage this? Is it a bug/limitation/intended? If it's going to stick around like this for a while is there any workaround for people who run into it? Thanks!

@ahejlsberg
Copy link
Member

I'm considering the best way to fix this. The core issue is that mapped types applied to array intersections do not produce arrays, and that's is a design limitation. However, the error in the repro is a regression, which is unfortunate.

The workaround is to remove the constraint from the type parameter and instead add it through an indirection:

type MustBeArray<T extends any[]> = T

type Hmm2<T> = T extends number[] ?
    MustBeArray<{ [I in keyof T]: 1 }> :
    never;

type Hmm<T extends any[]> = Hmm2<T>;

type X = Hmm<[3, 4, 5]>

@ahejlsberg ahejlsberg added Bug A bug in TypeScript Recent Regression This is a new regression just found in the last major/minor version of TypeScript. Fix Available A PR has been opened for this issue and removed Needs Investigation This issue needs a team member to investigate its status. labels Mar 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Recent Regression This is a new regression just found in the last major/minor version of TypeScript.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants