Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 11 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13861,7 +13861,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const typeVariable = getHomomorphicTypeVariable(type);
if (typeVariable && !type.declaration.nameType) {
const constraint = getConstraintOfTypeParameter(typeVariable);
if (constraint && isArrayType(constraint)) {
if (constraint && everyType(constraint, isArrayOrTupleType)) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In a way, this reverts the recent change from #52651 . That issue is now fixed by avoiding getting the apparent type of mapped types within getApparentTypeOfContextualType - you can see the change in the commit here

I think this is a better fix for that issue anyway, I just didn't figure it out earlier. Getting the "resolved" apparent type for mapped types with tuple constraint (and array) constraints is important for relationship checking. The mapped type that resolves~ to a tuple is usually "normalized" here, that allows tuple/array-oriented branches to relate things.

return instantiateType(type, prependTypeMapping(typeVariable, constraint, type.mapper));
}
}
Expand Down Expand Up @@ -21655,7 +21655,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return varianceResult;
}
}
else if (isReadonlyArrayType(target) ? isArrayOrTupleType(source) : isArrayType(target) && isTupleType(source) && !source.target.readonly) {
else if (isReadonlyArrayType(target) ? everyType(source, isArrayOrTupleType) : isArrayType(target) && everyType(source, t => isTupleType(t) && !t.target.readonly)) {
if (relation !== identityRelation) {
return isRelatedTo(getIndexTypeOfType(source, numberType) || anyType, getIndexTypeOfType(target, numberType) || anyType, RecursionFlags.Both, reportErrors);
}
Expand Down Expand Up @@ -29205,7 +29205,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
getContextualType(node, contextFlags);
const instantiatedType = instantiateContextualType(contextualType, node, contextFlags);
if (instantiatedType && !(contextFlags && contextFlags & ContextFlags.NoConstraints && instantiatedType.flags & TypeFlags.TypeVariable)) {
const apparentType = mapType(instantiatedType, getApparentType, /*noReductions*/ true);
const apparentType = mapType(
instantiatedType,
// When obtaining apparent type of *contextual* type we don't want to get apparent type of mapped types.
// That would evaluate mapped types with array or tuple type constraints too eagerly
// and thus it would prevent `getTypeOfPropertyOfContextualType` from obtaining per-position contextual type for elements of array literal expressions.
// Apparent type of other mapped types is already the mapped type itself so we can just avoid calling `getApparentType` here for all mapped types.
t => getObjectFlags(t) & ObjectFlags.Mapped ? t : getApparentType(t),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This might also need to handle intersections better, their mapped types parts should be left alone similarly. I don't have a test case to validate this at hand though so I refrained from making further changes to this. However, I think that I know how to write a sensible test case for this with the changes from this PR. So I would like to wait for one of them to land so I could tweak this part of the code and add the appropriate test case to the other PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

And I pushed out this extra fix here: 56f12bc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would you care to open a new PR for the further improvement? Since it comes with a test case proving it's utility, it looks pretty good - will just need to make sure the extended suites stay clean when that handling is in place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh, I thought that this would be linked better - this commit is already part of the #52062 :) it only made sense for me to add this change there as that PR was already concerned with intersected mapped types and thus I already had an idea what a test to write there for this patch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ahh, OK.

/*noReductions*/ true
);
return apparentType.flags & TypeFlags.Union && isObjectLiteralExpression(node) ? discriminateContextualTypeByObjectMembers(node, apparentType as UnionType) :
apparentType.flags & TypeFlags.Union && isJsxAttributes(node) ? discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType) :
apparentType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
tests/cases/compiler/mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts(9,9): error TS2322: Type 'HomomorphicMappedType<T>' is not assignable to type 'any[]'.


==== tests/cases/compiler/mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts (1 errors) ====
type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }

function test1<T extends [number] | [string]>(args: T) {
const arr: any[] = [] as HomomorphicMappedType<T>
const arr2: readonly any[] = [] as HomomorphicMappedType<T>
}

function test2<T extends [number] | readonly [string]>(args: T) {
const arr: any[] = [] as HomomorphicMappedType<T> // error
~~~
!!! error TS2322: Type 'HomomorphicMappedType<T>' is not assignable to type 'any[]'.
const arr2: readonly any[] = [] as HomomorphicMappedType<T>
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
=== tests/cases/compiler/mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts ===
type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 0))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 27))
>P : Symbol(P, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 35))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 27))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 27))
>P : Symbol(P, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 35))

function test1<T extends [number] | [string]>(args: T) {
>test1 : Symbol(test1, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 88))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 2, 15))
>args : Symbol(args, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 2, 46))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 2, 15))

const arr: any[] = [] as HomomorphicMappedType<T>
>arr : Symbol(arr, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 3, 7))
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 0))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 2, 15))

const arr2: readonly any[] = [] as HomomorphicMappedType<T>
>arr2 : Symbol(arr2, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 4, 7))
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 0))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 2, 15))
}

function test2<T extends [number] | readonly [string]>(args: T) {
>test2 : Symbol(test2, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 5, 1))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 7, 15))
>args : Symbol(args, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 7, 55))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 7, 15))

const arr: any[] = [] as HomomorphicMappedType<T> // error
>arr : Symbol(arr, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 8, 7))
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 0))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 7, 15))

const arr2: readonly any[] = [] as HomomorphicMappedType<T>
>arr2 : Symbol(arr2, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 9, 7))
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 0, 0))
>T : Symbol(T, Decl(mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts, 7, 15))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
=== tests/cases/compiler/mappedTypeUnionConstrainTupleTreatedAsArrayLike.ts ===
type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }
>HomomorphicMappedType : HomomorphicMappedType<T>

function test1<T extends [number] | [string]>(args: T) {
>test1 : <T extends [number] | [string]>(args: T) => void
>args : T

const arr: any[] = [] as HomomorphicMappedType<T>
>arr : any[]
>[] as HomomorphicMappedType<T> : HomomorphicMappedType<T>
>[] : []

const arr2: readonly any[] = [] as HomomorphicMappedType<T>
>arr2 : readonly any[]
>[] as HomomorphicMappedType<T> : HomomorphicMappedType<T>
>[] : []
}

function test2<T extends [number] | readonly [string]>(args: T) {
>test2 : <T extends [number] | readonly [string]>(args: T) => void
>args : T

const arr: any[] = [] as HomomorphicMappedType<T> // error
>arr : any[]
>[] as HomomorphicMappedType<T> : HomomorphicMappedType<T>
>[] : []

const arr2: readonly any[] = [] as HomomorphicMappedType<T>
>arr2 : readonly any[]
>[] as HomomorphicMappedType<T> : HomomorphicMappedType<T>
>[] : []
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
=== tests/cases/compiler/restParamUsingMappedTypeOverUnionConstraint.ts ===
// repro 29919#issuecomment-470948453

type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 0, 0))
>T : Symbol(T, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 2, 27))
>P : Symbol(P, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 2, 35))
>T : Symbol(T, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 2, 27))
>T : Symbol(T, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 2, 27))
>P : Symbol(P, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 2, 35))

declare function test<T extends [number] | [string]>(
>test : Symbol(test, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 2, 88))
>T : Symbol(T, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 4, 22))

args: T,
>args : Symbol(args, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 4, 53))
>T : Symbol(T, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 4, 22))

fn: (...args: HomomorphicMappedType<T>) => void
>fn : Symbol(fn, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 5, 10))
>args : Symbol(args, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 6, 7))
>HomomorphicMappedType : Symbol(HomomorphicMappedType, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 0, 0))
>T : Symbol(T, Decl(restParamUsingMappedTypeOverUnionConstraint.ts, 4, 22))

): void

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
=== tests/cases/compiler/restParamUsingMappedTypeOverUnionConstraint.ts ===
// repro 29919#issuecomment-470948453

type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }
>HomomorphicMappedType : HomomorphicMappedType<T>

declare function test<T extends [number] | [string]>(
>test : <T extends [number] | [string]>(args: T, fn: (...args: HomomorphicMappedType<T>) => void) => void

args: T,
>args : T

fn: (...args: HomomorphicMappedType<T>) => void
>fn : (...args: HomomorphicMappedType<T>) => void
>args : HomomorphicMappedType<T>

): void

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @noEmit: true
// @strict: true

type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }

function test1<T extends [number] | [string]>(args: T) {
const arr: any[] = [] as HomomorphicMappedType<T>
const arr2: readonly any[] = [] as HomomorphicMappedType<T>
}

function test2<T extends [number] | readonly [string]>(args: T) {
const arr: any[] = [] as HomomorphicMappedType<T> // error
const arr2: readonly any[] = [] as HomomorphicMappedType<T>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @noEmit: true
// @strict: true

// repro 29919#issuecomment-470948453

type HomomorphicMappedType<T> = { [P in keyof T]: T[P] extends string ? boolean : null }

declare function test<T extends [number] | [string]>(
args: T,
fn: (...args: HomomorphicMappedType<T>) => void
): void