Skip to content

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

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

Merged
merged 17 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.

return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable, t, mapper));
}
if (isGenericTupleType(t)) {
Expand Down
43 changes: 41 additions & 2 deletions tests/baselines/reference/mappedTypeWithAny.errors.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
tests/cases/conformance/types/mapped/mappedTypeWithAny.ts(23,16): error TS2339: Property 'notAValue' does not exist on type 'Data'.
tests/cases/conformance/types/mapped/mappedTypeWithAny.ts(45,5): error TS2740: Type 'Objectish<any>' is missing the following properties from type 'any[]': length, pop, push, concat, and 16 more.
tests/cases/conformance/types/mapped/mappedTypeWithAny.ts(46,5): error TS2322: Type 'Objectish<any>' is not assignable to type 'any[]'.
tests/cases/conformance/types/mapped/mappedTypeWithAny.ts(53,5): error TS2322: Type 'string[]' is not assignable to type '[any, any]'.
Target requires 2 element(s) but source may have fewer.


==== tests/cases/conformance/types/mapped/mappedTypeWithAny.ts (1 errors) ====
==== tests/cases/conformance/types/mapped/mappedTypeWithAny.ts (4 errors) ====
type Item = { value: string };
type ItemMap<T> = { [P in keyof T]: Item };

Expand All @@ -28,4 +32,39 @@ tests/cases/conformance/types/mapped/mappedTypeWithAny.ts(23,16): error TS2339:
~~~~~~~~~
!!! error TS2339: Property 'notAValue' does not exist on type 'Data'.
}


// Issue #46169.
// We want mapped types whose constraint is `keyof T` to
// map over `any` differently, depending on whether `T`
// is constrained to array and tuple types.
type Arrayish<T extends unknown[]> = { [K in keyof T]: T[K] };
type Objectish<T extends unknown> = { [K in keyof T]: T[K] };

// When a mapped type whose constraint is `keyof T` is instantiated,
// `T` may be instantiated with a `U` which is constrained to
// array and tuple types. *Ideally*, when `U` is later instantiated with `any`,
// the result should also be some sort of array; however, at the moment we don't seem
// to have an easy way to preserve that information. More than just that, it would be
// inconsistent for two instantiations of `Objectish<any>` to produce different outputs
// depending on the usage-site. As a result, `IndirectArrayish` does not act like `Arrayish`.
type IndirectArrayish<U extends unknown[]> = Objectish<U>;

function bar(arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>) {
let arr: any[];
arr = arrayish;
arr = objectish;
~~~
!!! error TS2740: Type 'Objectish<any>' is missing the following properties from type 'any[]': length, pop, push, concat, and 16 more.
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?

}

declare function stringifyArray<T extends readonly any[]>(arr: T): { -readonly [K in keyof T]: string };
let abc: any[] = stringifyArray(void 0 as any);

declare function stringifyPair<T extends readonly [any, any]>(arr: T): { -readonly [K in keyof T]: string };
let def: [any, any] = stringifyPair(void 0 as any);
~~~
!!! error TS2322: Type 'string[]' is not assignable to type '[any, any]'.
!!! error TS2322: Target requires 2 element(s) but source may have fewer.
54 changes: 53 additions & 1 deletion tests/baselines/reference/mappedTypeWithAny.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,50 @@ for (let id in z) {
let data = z[id];
let x = data.notAValue; // Error
}


// Issue #46169.
// We want mapped types whose constraint is `keyof T` to
// map over `any` differently, depending on whether `T`
// is constrained to array and tuple types.
type Arrayish<T extends unknown[]> = { [K in keyof T]: T[K] };
type Objectish<T extends unknown> = { [K in keyof T]: T[K] };

// When a mapped type whose constraint is `keyof T` is instantiated,
// `T` may be instantiated with a `U` which is constrained to
// array and tuple types. *Ideally*, when `U` is later instantiated with `any`,
// the result should also be some sort of array; however, at the moment we don't seem
// to have an easy way to preserve that information. More than just that, it would be
// inconsistent for two instantiations of `Objectish<any>` to produce different outputs
// depending on the usage-site. As a result, `IndirectArrayish` does not act like `Arrayish`.
type IndirectArrayish<U extends unknown[]> = Objectish<U>;

function bar(arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>) {
let arr: any[];
arr = arrayish;
arr = objectish;
arr = indirectArrayish;
}

declare function stringifyArray<T extends readonly any[]>(arr: T): { -readonly [K in keyof T]: string };
let abc: any[] = stringifyArray(void 0 as any);

declare function stringifyPair<T extends readonly [any, any]>(arr: T): { -readonly [K in keyof T]: string };
let def: [any, any] = stringifyPair(void 0 as any);

//// [mappedTypeWithAny.js]
"use strict";
for (var id in z) {
var data = z[id];
var x = data.notAValue; // Error
}
function bar(arrayish, objectish, indirectArrayish) {
var arr;
arr = arrayish;
arr = objectish;
arr = indirectArrayish;
}
var abc = stringifyArray(void 0);
var def = stringifyPair(void 0);


//// [mappedTypeWithAny.d.ts]
Expand Down Expand Up @@ -58,3 +94,19 @@ declare type StrictDataMap<T> = {
[P in keyof T]: Data;
};
declare let z: StrictDataMap<any>;
declare type Arrayish<T extends unknown[]> = {
[K in keyof T]: T[K];
};
declare type Objectish<T extends unknown> = {
[K in keyof T]: T[K];
};
declare type IndirectArrayish<U extends unknown[]> = Objectish<U>;
declare function bar(arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>): void;
declare function stringifyArray<T extends readonly any[]>(arr: T): {
-readonly [K in keyof T]: string;
};
declare let abc: any[];
declare function stringifyPair<T extends readonly [any, any]>(arr: T): {
-readonly [K in keyof T]: string;
};
declare let def: [any, any];
82 changes: 82 additions & 0 deletions tests/baselines/reference/mappedTypeWithAny.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,85 @@ for (let id in z) {
>data : Symbol(data, Decl(mappedTypeWithAny.ts, 21, 5))
}

// Issue #46169.
// We want mapped types whose constraint is `keyof T` to
// map over `any` differently, depending on whether `T`
// is constrained to array and tuple types.
type Arrayish<T extends unknown[]> = { [K in keyof T]: T[K] };
>Arrayish : Symbol(Arrayish, Decl(mappedTypeWithAny.ts, 23, 1))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 29, 14))
>K : Symbol(K, Decl(mappedTypeWithAny.ts, 29, 40))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 29, 14))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 29, 14))
>K : Symbol(K, Decl(mappedTypeWithAny.ts, 29, 40))

type Objectish<T extends unknown> = { [K in keyof T]: T[K] };
>Objectish : Symbol(Objectish, Decl(mappedTypeWithAny.ts, 29, 62))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 30, 15))
>K : Symbol(K, Decl(mappedTypeWithAny.ts, 30, 39))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 30, 15))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 30, 15))
>K : Symbol(K, Decl(mappedTypeWithAny.ts, 30, 39))

// When a mapped type whose constraint is `keyof T` is instantiated,
// `T` may be instantiated with a `U` which is constrained to
// array and tuple types. *Ideally*, when `U` is later instantiated with `any`,
// the result should also be some sort of array; however, at the moment we don't seem
// to have an easy way to preserve that information. More than just that, it would be
// inconsistent for two instantiations of `Objectish<any>` to produce different outputs
// depending on the usage-site. As a result, `IndirectArrayish` does not act like `Arrayish`.
type IndirectArrayish<U extends unknown[]> = Objectish<U>;
>IndirectArrayish : Symbol(IndirectArrayish, Decl(mappedTypeWithAny.ts, 30, 61))
>U : Symbol(U, Decl(mappedTypeWithAny.ts, 39, 22))
>Objectish : Symbol(Objectish, Decl(mappedTypeWithAny.ts, 29, 62))
>U : Symbol(U, Decl(mappedTypeWithAny.ts, 39, 22))

function bar(arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>) {
>bar : Symbol(bar, Decl(mappedTypeWithAny.ts, 39, 58))
>arrayish : Symbol(arrayish, Decl(mappedTypeWithAny.ts, 41, 13))
>Arrayish : Symbol(Arrayish, Decl(mappedTypeWithAny.ts, 23, 1))
>objectish : Symbol(objectish, Decl(mappedTypeWithAny.ts, 41, 37))
>Objectish : Symbol(Objectish, Decl(mappedTypeWithAny.ts, 29, 62))
>indirectArrayish : Symbol(indirectArrayish, Decl(mappedTypeWithAny.ts, 41, 64))
>IndirectArrayish : Symbol(IndirectArrayish, Decl(mappedTypeWithAny.ts, 30, 61))

let arr: any[];
>arr : Symbol(arr, Decl(mappedTypeWithAny.ts, 42, 7))

arr = arrayish;
>arr : Symbol(arr, Decl(mappedTypeWithAny.ts, 42, 7))
>arrayish : Symbol(arrayish, Decl(mappedTypeWithAny.ts, 41, 13))

arr = objectish;
>arr : Symbol(arr, Decl(mappedTypeWithAny.ts, 42, 7))
>objectish : Symbol(objectish, Decl(mappedTypeWithAny.ts, 41, 37))

arr = indirectArrayish;
>arr : Symbol(arr, Decl(mappedTypeWithAny.ts, 42, 7))
>indirectArrayish : Symbol(indirectArrayish, Decl(mappedTypeWithAny.ts, 41, 64))
}

declare function stringifyArray<T extends readonly any[]>(arr: T): { -readonly [K in keyof T]: string };
>stringifyArray : Symbol(stringifyArray, Decl(mappedTypeWithAny.ts, 46, 1))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 48, 32))
>arr : Symbol(arr, Decl(mappedTypeWithAny.ts, 48, 58))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 48, 32))
>K : Symbol(K, Decl(mappedTypeWithAny.ts, 48, 80))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 48, 32))

let abc: any[] = stringifyArray(void 0 as any);
>abc : Symbol(abc, Decl(mappedTypeWithAny.ts, 49, 3))
>stringifyArray : Symbol(stringifyArray, Decl(mappedTypeWithAny.ts, 46, 1))

declare function stringifyPair<T extends readonly [any, any]>(arr: T): { -readonly [K in keyof T]: string };
>stringifyPair : Symbol(stringifyPair, Decl(mappedTypeWithAny.ts, 49, 47))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 51, 31))
>arr : Symbol(arr, Decl(mappedTypeWithAny.ts, 51, 62))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 51, 31))
>K : Symbol(K, Decl(mappedTypeWithAny.ts, 51, 84))
>T : Symbol(T, Decl(mappedTypeWithAny.ts, 51, 31))

let def: [any, any] = stringifyPair(void 0 as any);
>def : Symbol(def, Decl(mappedTypeWithAny.ts, 52, 3))
>stringifyPair : Symbol(stringifyPair, Decl(mappedTypeWithAny.ts, 49, 47))

69 changes: 69 additions & 0 deletions tests/baselines/reference/mappedTypeWithAny.types
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,72 @@ for (let id in z) {
>notAValue : any
}

// Issue #46169.
// We want mapped types whose constraint is `keyof T` to
// map over `any` differently, depending on whether `T`
// is constrained to array and tuple types.
type Arrayish<T extends unknown[]> = { [K in keyof T]: T[K] };
>Arrayish : Arrayish<T>

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

// When a mapped type whose constraint is `keyof T` is instantiated,
// `T` may be instantiated with a `U` which is constrained to
// array and tuple types. *Ideally*, when `U` is later instantiated with `any`,
// the result should also be some sort of array; however, at the moment we don't seem
// to have an easy way to preserve that information. More than just that, it would be
// inconsistent for two instantiations of `Objectish<any>` to produce different outputs
// depending on the usage-site. As a result, `IndirectArrayish` does not act like `Arrayish`.
type IndirectArrayish<U extends unknown[]> = Objectish<U>;
>IndirectArrayish : Objectish<U>

function bar(arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>) {
>bar : (arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>) => void
>arrayish : any[]
>objectish : Objectish<any>
>indirectArrayish : Objectish<any>

let arr: any[];
>arr : any[]

arr = arrayish;
>arr = arrayish : any[]
>arr : any[]
>arrayish : any[]

arr = objectish;
>arr = objectish : Objectish<any>
>arr : any[]
>objectish : Objectish<any>

arr = indirectArrayish;
>arr = indirectArrayish : Objectish<any>
>arr : any[]
>indirectArrayish : Objectish<any>
}

declare function stringifyArray<T extends readonly any[]>(arr: T): { -readonly [K in keyof T]: string };
>stringifyArray : <T extends readonly any[]>(arr: T) => { -readonly [K in keyof T]: string; }
>arr : T

let abc: any[] = stringifyArray(void 0 as any);
>abc : any[]
>stringifyArray(void 0 as any) : string[]
>stringifyArray : <T extends readonly any[]>(arr: T) => { -readonly [K in keyof T]: string; }
>void 0 as any : any
>void 0 : undefined
>0 : 0

declare function stringifyPair<T extends readonly [any, any]>(arr: T): { -readonly [K in keyof T]: string };
>stringifyPair : <T extends readonly [any, any]>(arr: T) => { -readonly [K in keyof T]: string; }
>arr : T

let def: [any, any] = stringifyPair(void 0 as any);
>def : [any, any]
>stringifyPair(void 0 as any) : string[]
>stringifyPair : <T extends readonly [any, any]>(arr: T) => { -readonly [K in keyof T]: string; }
>void 0 as any : any
>void 0 : undefined
>0 : 0

19 changes: 19 additions & 0 deletions tests/baselines/reference/promiseAllOnAny01.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
=== tests/cases/compiler/promiseAllOnAny01.ts ===
async function foo(x: any) {
>foo : Symbol(foo, Decl(promiseAllOnAny01.ts, 0, 0))
>x : Symbol(x, Decl(promiseAllOnAny01.ts, 0, 19))

let abc = await Promise.all(x);
>abc : Symbol(abc, Decl(promiseAllOnAny01.ts, 1, 7))
>Promise.all : Symbol(PromiseConstructor.all, Decl(lib.es2015.promise.d.ts, --, --))
>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --))
>all : Symbol(PromiseConstructor.all, Decl(lib.es2015.promise.d.ts, --, --))
>x : Symbol(x, Decl(promiseAllOnAny01.ts, 0, 19))

let result: any[] = abc;
>result : Symbol(result, Decl(promiseAllOnAny01.ts, 2, 7))
>abc : Symbol(abc, Decl(promiseAllOnAny01.ts, 1, 7))

return result;
>result : Symbol(result, Decl(promiseAllOnAny01.ts, 2, 7))
}
21 changes: 21 additions & 0 deletions tests/baselines/reference/promiseAllOnAny01.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
=== tests/cases/compiler/promiseAllOnAny01.ts ===
async function foo(x: any) {
>foo : (x: any) => Promise<any[]>
>x : any

let abc = await Promise.all(x);
>abc : any[]
>await Promise.all(x) : any[]
>Promise.all(x) : Promise<any[]>
>Promise.all : <T extends readonly unknown[] | []>(values: T) => Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>
>Promise : PromiseConstructor
>all : <T extends readonly unknown[] | []>(values: T) => Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>
>x : any

let result: any[] = abc;
>result : any[]
>abc : any[]

return result;
>result : any[]
}
8 changes: 8 additions & 0 deletions tests/cases/compiler/promiseAllOnAny01.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @noEmit: true
// @lib: es5,es2015.promise

async function foo(x: any) {
let abc = await Promise.all(x);
let result: any[] = abc;
return result;
}
29 changes: 29 additions & 0 deletions tests/cases/conformance/types/mapped/mappedTypeWithAny.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,32 @@ for (let id in z) {
let data = z[id];
let x = data.notAValue; // Error
}

// Issue #46169.
// We want mapped types whose constraint is `keyof T` to
// map over `any` differently, depending on whether `T`
// is constrained to array and tuple types.
type Arrayish<T extends unknown[]> = { [K in keyof T]: T[K] };
type Objectish<T extends unknown> = { [K in keyof T]: T[K] };

// When a mapped type whose constraint is `keyof T` is instantiated,
// `T` may be instantiated with a `U` which is constrained to
// array and tuple types. *Ideally*, when `U` is later instantiated with `any`,
// the result should also be some sort of array; however, at the moment we don't seem
// to have an easy way to preserve that information. More than just that, it would be
// inconsistent for two instantiations of `Objectish<any>` to produce different outputs
// depending on the usage-site. As a result, `IndirectArrayish` does not act like `Arrayish`.
type IndirectArrayish<U extends unknown[]> = Objectish<U>;

function bar(arrayish: Arrayish<any>, objectish: Objectish<any>, indirectArrayish: IndirectArrayish<any>) {
let arr: any[];
arr = arrayish;
arr = objectish;
arr = indirectArrayish;
}

declare function stringifyArray<T extends readonly any[]>(arr: T): { -readonly [K in keyof T]: string };
let abc: any[] = stringifyArray(void 0 as any);

declare function stringifyPair<T extends readonly [any, any]>(arr: T): { -readonly [K in keyof T]: string };
let def: [any, any] = stringifyPair(void 0 as any);