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

Check for array types when instantiating mapped type constraints with any #46218

Merged
merged 17 commits into from
Oct 27, 2021

Conversation

DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Oct 5, 2021

This introduces a change to mapped types so that homomorphic-like mapped types play better with any.

Today, when instantiating a mapped types on keyof T, an instantiation of T with an array-like type behaves differently than with other object types. This allows us to reuse the same mapped types syntax with different kinds of types, but forces us to make a decision when we've hit a type like any. When instantiating with any, we don't consider it to be array-like in any way, and we produce { [x: string]: any }.

type Foo<T extends unknown> = { [K in keyof T]: T[K] };

type X = Foo<any>; // { [x: string]: any }

This is fine in the above example, but is undesirable when constraints would otherwise only allow array-like types.

type Foo<T extends unknown[]> = { [K in keyof T]: T[K] };
//                 ^^^^^^^^^
// Now we're dealing with an array constraint.

type X = Foo<any>; // { [x: string]: any }

The constraints imply that the above mapped type should always produce an array; however, any throws a wrench in the equation. This change introduces a new rule to account for this strange behavior.

Instantiating a mapped type { [K in keyof T]: ... } now produces any[] if the instantiation of T is any, and if T is constrained by only array and tuple types.

This provides better behavior for Promise.all when passing in an argument of type any instead of an array.

Fixes #46169

@typescript-bot typescript-bot added the For Milestone Bug PRs that fix a bug with a specific milestone label Oct 5, 2021
@DanielRosenwasser
Copy link
Member Author

@typescript-bot pack this
@typescript-bot test this
@typescript-bot user test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 5, 2021

Heya @DanielRosenwasser, I've started to run the parallelized community code test suite on this PR at 99d9b56. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 5, 2021

Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at 99d9b56. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 5, 2021

Heya @DanielRosenwasser, I've started to run the extended test suite on this PR at 99d9b56. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 5, 2021

Hey @DanielRosenwasser, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/112435/artifacts?artifactName=tgz&fileId=77537E9D55CD639A77C2615E5C9FBC90336536082CD0571C13122F50CD35A6B302&fileName=/typescript-4.5.0-insiders.20211005.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@4.5.0-pr-46218-4".;

@typescript-bot
Copy link
Collaborator

The user suite test run you requested has finished and failed. I've opened a PR with the baseline diff from master.

arr = indirectArrayish;
~~~
!!! error TS2322: Type 'Objectish<any>' is not assignable to type 'any[]'.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intent was that IndirectArrayish<U> should be a mapped object type that has a type variable constraint of unknown[]; it seems like that might not be the case, so I actually might need some help here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'd need a new assignability rule for mapped types matching that pattern to accomplish that.

Copy link
Member Author

@DanielRosenwasser DanielRosenwasser Oct 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you're saying - that you need to have something recursive that checks if you're arrayish or a generic mapped type that would produce an array result.

function mappedTypeConstraintProducesArrayResult(type: Type) {
    if (isArrayType(type) || isTupleType(type)) return true;
    if (type.flags & TypeFlags.Union) return everyType(type, mappedTypeConstraintProducesArrayResult);

    const typeVariable = type.flags & TypeFlags.MappedType && getHomomorphicTypeVariable(type as MappedType);
    if (typeVariable && !type.declaration.nameType) {
        const constraint = getConstraintOfTypeParameter(typeVariable);
        return !!(constraint && mappedTypeConstraintProducesArrayResult(constraint));
    }
    return false;
}

But IndirectArrayish<any> and Objectish<any> aren't generic mapped types, are they?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh, I was thinking of something like relating a generic IndirectArrayish<U> to a U[]. In this case, I don't think we can distinguish between a Objectish<any> and an IndirectArrayish<any> since they're exactly equivalent - the "is this constrained to arrayish types only" check you have is purely syntactic (it only cares if the mapped type was both declared homomorphic and that variable is array-constrained) - it can't pick up information from wrapping declarations or anything.

Copy link
Member Author

@DanielRosenwasser DanielRosenwasser Oct 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the intent here was that when we declared IndirectArrayish<U extends unknown[]>, we'd end up with a fresh generic mapped type whose type variable (U) has the array constraint. Is that not what's happening?

@@ -16515,7 +16515,8 @@ namespace ts {
return mapTypeWithAlias(getReducedType(mappedTypeVariable), t => {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && !isErrorType(t)) {
if (!type.declaration.nameType) {
if (isArrayType(t)) {
let constraint;
if (isArrayType(t) || (t.flags & TypeFlags.Any) && (constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, or(isArrayType, isTupleType))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isArrayType(t) || (t.flags & TypeFlags.Any) && (constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, or(isArrayType, isTupleType))) {
if (isArrayType(t) || (t.flags & TypeFlags.Any) && (constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, isArrayOrTupleLikeType)) {

? Or is including the x-like types (length properties and 0 properties) going to mess up the behavior in a case we care about? I'm thinking passing any to a T extends {length: number} may also justify an array mapping rather than a string index signature.

Copy link
Member Author

@DanielRosenwasser DanielRosenwasser Oct 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about the tuple-like type thing, and that's one concern - I'm already not 100% convinced that that would always be desirable. The bigger concern is the implementation of isArrayLikeType:

return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType);

which I think would consider any an array-like type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw any in a constraint position is eagerly replaced with unknown nowadays (so it behaves as a proper top type constraint and not as an odd anyish thing) so it shouldn't be an issue I don't think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that might be fine, but for now there's some precedent in getResolvedApparentTypeOfMappedType for the current behavior.

@treybrisbane
Copy link

I don't think it's quite the same, but is this at all related to (or will it have any impact on) #27995?

@DanielRosenwasser
Copy link
Member Author

I don't think so - this only affects situations when you have a mapped type over T and instantiate with any in place of T.

@ahejlsberg
Copy link
Member

The basic change looks good to me and I don't think it's necessary (or even feasible) to check for "arrayishness" in the indirect case. However, the new behavior isn't right for type parameters constrained to tuple types since any[] is not assignable to any tuple types. For those, we really should produce an [any, any, ...] tuple with the appropriate arity, but it gets complicated to deal with optional, rest, and variadic elements in constraints. I think a good middle ground might be to limit the new behavior to cases where every type parameter constraint is an array type and just not do anything special for tuple type constraints.

@DanielRosenwasser
Copy link
Member Author

Sorry I missed your comment - I agree, and I've updated the PR to only act specially on array types. I've also updated the comment in the test case to explain some of the limitations/concerns we talked about.

@DanielRosenwasser
Copy link
Member Author

@ahejlsberg just hit a problem - the whole motivation of the original logic was that the type parameter of Promise.all is constrained to readonly unknown[] | [] - which means that only checking if you have an array type is insufficient.

@DanielRosenwasser
Copy link
Member Author

I'm inclined to go back to the original proposal; alternatively, I can change the current implementation to check if the union type contains only array or tuple types, and contains at least one array type.

@andrewbranch
Copy link
Member

What is “the original proposal”?

if the union type contains only array or tuple types, and contains at least one array type.

This sounds weirdly specific, but not terrible.

@DanielRosenwasser
Copy link
Member Author

The original proposal is that we just check if all types are array-like or tuple-like - basically the behavior prior to this commit f01ae16

@DanielRosenwasser
Copy link
Member Author

Latest commits (46283e2) brings that back.

@andrewbranch
Copy link
Member

@typescript-bot pack this

I know @ahejlsberg had concerns about the correctness of this for tuple types in #46218 (comment).

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 27, 2021

Heya @andrewbranch, I've started to run the tarball bundle task on this PR at b3492a9. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 27, 2021

Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/113976/artifacts?artifactName=tgz&fileId=801EF1FF477137D59C0165CE8B9A79EE04191A7B7F278368A4FC4753D24A189202&fileName=/typescript-4.5.0-insiders.20211027.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@4.5.0-pr-46218-18".;

@ahejlsberg
Copy link
Member

@DanielRosenwasser Am I confused, or did you first implement the change I suggested, and then revert again?

@ahejlsberg
Copy link
Member

@DanielRosenwasser Ah, I see your reason here. In that case I'm fine with the your original behavior, since our old behavior for tuples wasn't right either.

@DanielRosenwasser DanielRosenwasser merged commit f494742 into main Oct 27, 2021
@DanielRosenwasser DanielRosenwasser deleted the mappedTypesOnAny branch October 27, 2021 22:03
@DanielRosenwasser
Copy link
Member Author

Thanks for the reviews!

@sandersn
Copy link
Member

This breaks ramda types on DT. I'll file a bug.

mprobst pushed a commit to mprobst/TypeScript that referenced this pull request Jan 10, 2022
… `any` (microsoft#46218)

* Added/updated tests.

* Accepted baselines.

* Update test.

* Update instantiateMappedType to work specially when 'any' replaced an array.

* Accepted baselines.

* Ensure check works when constraint is a union of arrayish types, just like in `Promise.all`.

* Accepted baselines.

* Update test for indirect instantiation of a mapped type.

* Accepted baselines.

* Update test comment.

* Accepted baselines.

* Added tuple test case.

* Accepted baselines.

* Don't add special behavior for tuples.

* Accepted baselines.

* Revert "Don't add special behavior for tuples."

This reverts commit f01ae16.

* Accepted baselines.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Promise.all on any produces Promise<Record<string, any>>
8 participants