Skip to content

bugfix: homomorphic mapped types when T is non-generic, solves 27995 #48433

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
43 changes: 35 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13353,6 +13353,17 @@ namespace ts {
typeParameter.constraint = getInferredTypeParameterConstraint(typeParameter) || noConstraintType;
}
else {
// Detect is the constraint is for a homomorphic mapped type to a tuple and in case return a literal union of the used tuple keys
if (constraintDeclaration.parent && constraintDeclaration.parent.parent && constraintDeclaration.parent.parent.kind === SyntaxKind.MappedType) {
const mappedTypeNode = constraintDeclaration.parent.parent as MappedTypeNode;
if (!mappedTypeNode.nameType && mappedTypeNode.typeParameter.constraint && isTypeOperatorNode(mappedTypeNode.typeParameter.constraint) && mappedTypeNode.typeParameter.constraint.operator === SyntaxKind.KeyOfKeyword) {
const keyOfTarget = getTypeFromTypeNode(mappedTypeNode.typeParameter.constraint.type);
if (isTupleType(keyOfTarget)) {
typeParameter.constraint = getUnionType(map(getTypeArguments(keyOfTarget), (_, i) => getStringLiteralType("" + i)));
return typeParameter.constraint;
}
}
}
let type = getTypeFromTypeNode(constraintDeclaration);
if (type.flags & TypeFlags.Any && !isErrorType(type)) { // Allow errorType to propegate to keep downstream errors suppressed
// use keyofConstraintType as the base constraint for mapped type key constraints (unknown isn;t assignable to that, but `any` was),
Expand Down Expand Up @@ -15848,6 +15859,19 @@ namespace ts {
// Eagerly resolve the constraint type which forces an error if the constraint type circularly
// references itself through one or more type aliases.
getConstraintTypeFromMappedType(type);
Copy link
Contributor

Choose a reason for hiding this comment

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

do you perhaps understand why this isn't sort of handled within getConstraintTypeFromMappedType? I recall we were trying to investigate that

Copy link
Author

Choose a reason for hiding this comment

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

So getConstraintTypeFromMappedType is delegating to getConstraintOfTypeParameter the job of getting the constraint type and additionaly sets it in the mapped type, here I have 2 choices either extend getConstraintTypeFromMappedType to detect the condition and eventually mutate the constraint of the type parameter or to extend the logic of getConstraintOfTypeParameter to check if the constraint is for a homomorphic mapped type. The function getConstraintOfTypeParameter is also directly invoked when checking index expressions like A[k] where getConstraintTypeFromMappedType isn't, the function returns the already resolved constraint if there so in theory it could still work but I felt more appropriate to have this logic in getConstraintOfTypeParameter as the lowest common

// Detect if the mapped type should be homomorphic to a tuple by checking the declaration of the constraint if it contains a keyof over a tuple
if (!node.nameType && node.typeParameter.constraint && isTypeOperatorNode(node.typeParameter.constraint) && node.typeParameter.constraint.operator === SyntaxKind.KeyOfKeyword) {
const keyOfTarget = getTypeFromTypeNode(node.typeParameter.constraint.type);
if (isTupleType(keyOfTarget)) {
// Instantiate the mapped type over a tuple with an identity mapper
const instantiatedTupleMappedType = instantiateMappedTupleType(
keyOfTarget,
type,
makeFunctionTypeMapper(identity)
);
links.resolvedType = instantiatedTupleMappedType;
}
}
}
return links.resolvedType;
}
Expand Down Expand Up @@ -35579,14 +35603,17 @@ namespace ts {
reportImplicitAny(node, anyType);
}

const type = getTypeFromMappedTypeNode(node) as MappedType;
const nameType = getNameTypeFromMappedType(type);
if (nameType) {
checkTypeAssignableTo(nameType, keyofConstraintType, node.nameType);
}
else {
const constraintType = getConstraintTypeFromMappedType(type);
checkTypeAssignableTo(constraintType, keyofConstraintType, getEffectiveConstraintOfTypeParameter(node.typeParameter));
const type = getTypeFromMappedTypeNode(node);
// Continue to check if the type returned is a mapped type, that means it wasn't resolved to a homomorphic tuple type
Copy link
Contributor

Choose a reason for hiding this comment

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

where does this typechecking for a homomorphic mapped type happen now? in the added instantiateMappedTupleType call?

Copy link
Author

Choose a reason for hiding this comment

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

So in case the returned type is a tuple type (which I now realize it may be a simpler condition here) we don't have a mapped type to check, one condition is implicitly checked, as the type can only be homomorphic if it has no nameType (the AS rest component of the type parameter) the other on assignability of the constraint to the keyOfConstraintType should be once again implicit given the constraint is the keyof of a tuple BUT I may be wrong :)

if (type.flags & TypeFlags.Object && (type as ObjectType).objectFlags & ObjectFlags.Mapped) {
const nameType = getNameTypeFromMappedType(type as MappedType);
if (nameType) {
checkTypeAssignableTo(nameType, keyofConstraintType, node.nameType);
}
else {
const constraintType = getConstraintTypeFromMappedType(type as MappedType);
checkTypeAssignableTo(constraintType, keyofConstraintType, getEffectiveConstraintOfTypeParameter(node.typeParameter));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
tests/cases/compiler/mappedTypeConcreteTupleHomomorphism.ts(22,47): error TS2322: Type 'TupleOfNumbersAndObjects[K]' is not assignable to type 'string | number | bigint | boolean'.
Type '{} | 2 | 1' is not assignable to type 'string | number | bigint | boolean'.
Type '{}' is not assignable to type 'string | number | bigint | boolean'.


==== tests/cases/compiler/mappedTypeConcreteTupleHomomorphism.ts (1 errors) ====
type TupleOfNumbers = [1, 2]

type HomomorphicType = {
[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
}

const homomorphic: HomomorphicType = ['1', '2']

type GenericType<T> = {
[K in keyof T]: [K, T[K]]
}

type HomomorphicInstantiation = {
[K in keyof GenericType<['c', 'd', 'e']>]: 1
}

const d: HomomorphicInstantiation = [1, 1, 1]

type TupleOfNumbersAndObjects = [1, 2, {}]

type ShoulsErrorInAssignement = {
[K in keyof TupleOfNumbersAndObjects]: `${TupleOfNumbersAndObjects[K]}`
~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type 'TupleOfNumbersAndObjects[K]' is not assignable to type 'string | number | bigint | boolean'.
!!! error TS2322: Type '{} | 2 | 1' is not assignable to type 'string | number | bigint | boolean'.
!!! error TS2322: Type '{}' is not assignable to type 'string | number | bigint | boolean'.
}

// repro from #27995
type Foo = ['a', 'b'];

interface Bar {
a: string;
b: number;
}

type Baz = { [K in keyof Foo]: Bar[Foo[K]]; };

39 changes: 39 additions & 0 deletions tests/baselines/reference/mappedTypeConcreteTupleHomomorphism.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//// [mappedTypeConcreteTupleHomomorphism.ts]
type TupleOfNumbers = [1, 2]

type HomomorphicType = {
[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
}

const homomorphic: HomomorphicType = ['1', '2']

type GenericType<T> = {
[K in keyof T]: [K, T[K]]
}

type HomomorphicInstantiation = {
[K in keyof GenericType<['c', 'd', 'e']>]: 1
}

const d: HomomorphicInstantiation = [1, 1, 1]

type TupleOfNumbersAndObjects = [1, 2, {}]

type ShoulsErrorInAssignement = {
[K in keyof TupleOfNumbersAndObjects]: `${TupleOfNumbersAndObjects[K]}`
}

// repro from #27995
type Foo = ['a', 'b'];

interface Bar {
a: string;
b: number;
}

type Baz = { [K in keyof Foo]: Bar[Foo[K]]; };


//// [mappedTypeConcreteTupleHomomorphism.js]
var homomorphic = ['1', '2'];
var d = [1, 1, 1];
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
=== tests/cases/compiler/mappedTypeConcreteTupleHomomorphism.ts ===
type TupleOfNumbers = [1, 2]
>TupleOfNumbers : Symbol(TupleOfNumbers, Decl(mappedTypeConcreteTupleHomomorphism.ts, 0, 0))

type HomomorphicType = {
>HomomorphicType : Symbol(HomomorphicType, Decl(mappedTypeConcreteTupleHomomorphism.ts, 0, 28))

[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 3, 5))
>TupleOfNumbers : Symbol(TupleOfNumbers, Decl(mappedTypeConcreteTupleHomomorphism.ts, 0, 0))
>TupleOfNumbers : Symbol(TupleOfNumbers, Decl(mappedTypeConcreteTupleHomomorphism.ts, 0, 0))
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 3, 5))
}

const homomorphic: HomomorphicType = ['1', '2']
>homomorphic : Symbol(homomorphic, Decl(mappedTypeConcreteTupleHomomorphism.ts, 6, 5))
>HomomorphicType : Symbol(HomomorphicType, Decl(mappedTypeConcreteTupleHomomorphism.ts, 0, 28))

type GenericType<T> = {
>GenericType : Symbol(GenericType, Decl(mappedTypeConcreteTupleHomomorphism.ts, 6, 47))
>T : Symbol(T, Decl(mappedTypeConcreteTupleHomomorphism.ts, 8, 17))

[K in keyof T]: [K, T[K]]
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 9, 5))
>T : Symbol(T, Decl(mappedTypeConcreteTupleHomomorphism.ts, 8, 17))
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 9, 5))
>T : Symbol(T, Decl(mappedTypeConcreteTupleHomomorphism.ts, 8, 17))
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 9, 5))
}

type HomomorphicInstantiation = {
>HomomorphicInstantiation : Symbol(HomomorphicInstantiation, Decl(mappedTypeConcreteTupleHomomorphism.ts, 10, 1))

[K in keyof GenericType<['c', 'd', 'e']>]: 1
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 13, 5))
>GenericType : Symbol(GenericType, Decl(mappedTypeConcreteTupleHomomorphism.ts, 6, 47))
}

const d: HomomorphicInstantiation = [1, 1, 1]
>d : Symbol(d, Decl(mappedTypeConcreteTupleHomomorphism.ts, 16, 5))
>HomomorphicInstantiation : Symbol(HomomorphicInstantiation, Decl(mappedTypeConcreteTupleHomomorphism.ts, 10, 1))

type TupleOfNumbersAndObjects = [1, 2, {}]
>TupleOfNumbersAndObjects : Symbol(TupleOfNumbersAndObjects, Decl(mappedTypeConcreteTupleHomomorphism.ts, 16, 45))

type ShoulsErrorInAssignement = {
>ShoulsErrorInAssignement : Symbol(ShoulsErrorInAssignement, Decl(mappedTypeConcreteTupleHomomorphism.ts, 18, 42))

[K in keyof TupleOfNumbersAndObjects]: `${TupleOfNumbersAndObjects[K]}`
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 21, 5))
>TupleOfNumbersAndObjects : Symbol(TupleOfNumbersAndObjects, Decl(mappedTypeConcreteTupleHomomorphism.ts, 16, 45))
>TupleOfNumbersAndObjects : Symbol(TupleOfNumbersAndObjects, Decl(mappedTypeConcreteTupleHomomorphism.ts, 16, 45))
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 21, 5))
}

// repro from #27995
type Foo = ['a', 'b'];
>Foo : Symbol(Foo, Decl(mappedTypeConcreteTupleHomomorphism.ts, 22, 1))

interface Bar {
>Bar : Symbol(Bar, Decl(mappedTypeConcreteTupleHomomorphism.ts, 25, 22))

a: string;
>a : Symbol(Bar.a, Decl(mappedTypeConcreteTupleHomomorphism.ts, 27, 15))

b: number;
>b : Symbol(Bar.b, Decl(mappedTypeConcreteTupleHomomorphism.ts, 28, 14))
}

type Baz = { [K in keyof Foo]: Bar[Foo[K]]; };
>Baz : Symbol(Baz, Decl(mappedTypeConcreteTupleHomomorphism.ts, 30, 1))
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 32, 14))
>Foo : Symbol(Foo, Decl(mappedTypeConcreteTupleHomomorphism.ts, 22, 1))
>Bar : Symbol(Bar, Decl(mappedTypeConcreteTupleHomomorphism.ts, 25, 22))
>Foo : Symbol(Foo, Decl(mappedTypeConcreteTupleHomomorphism.ts, 22, 1))
>K : Symbol(K, Decl(mappedTypeConcreteTupleHomomorphism.ts, 32, 14))

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
=== tests/cases/compiler/mappedTypeConcreteTupleHomomorphism.ts ===
type TupleOfNumbers = [1, 2]
>TupleOfNumbers : TupleOfNumbers

type HomomorphicType = {
>HomomorphicType : ["1", "2"]

[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
}

const homomorphic: HomomorphicType = ['1', '2']
>homomorphic : ["1", "2"]
>['1', '2'] : ["1", "2"]
>'1' : "1"
>'2' : "2"

type GenericType<T> = {
>GenericType : GenericType<T>

[K in keyof T]: [K, T[K]]
}

type HomomorphicInstantiation = {
>HomomorphicInstantiation : [1, 1, 1]

[K in keyof GenericType<['c', 'd', 'e']>]: 1
}

const d: HomomorphicInstantiation = [1, 1, 1]
>d : [1, 1, 1]
>[1, 1, 1] : [1, 1, 1]
>1 : 1
>1 : 1
>1 : 1

type TupleOfNumbersAndObjects = [1, 2, {}]
>TupleOfNumbersAndObjects : TupleOfNumbersAndObjects

type ShoulsErrorInAssignement = {
>ShoulsErrorInAssignement : ["1", "2", string]

[K in keyof TupleOfNumbersAndObjects]: `${TupleOfNumbersAndObjects[K]}`
}

// repro from #27995
type Foo = ['a', 'b'];
>Foo : Foo

interface Bar {
a: string;
>a : string

b: number;
>b : number
}

type Baz = { [K in keyof Foo]: Bar[Foo[K]]; };
>Baz : [string, number]

33 changes: 33 additions & 0 deletions tests/cases/compiler/mappedTypeConcreteTupleHomomorphism.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type TupleOfNumbers = [1, 2]

type HomomorphicType = {
[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
}

const homomorphic: HomomorphicType = ['1', '2']

type GenericType<T> = {
[K in keyof T]: [K, T[K]]
}

type HomomorphicInstantiation = {
[K in keyof GenericType<['c', 'd', 'e']>]: 1
}

const d: HomomorphicInstantiation = [1, 1, 1]

type TupleOfNumbersAndObjects = [1, 2, {}]

type ShoulsErrorInAssignement = {
[K in keyof TupleOfNumbersAndObjects]: `${TupleOfNumbersAndObjects[K]}`
}

// repro from #27995
type Foo = ['a', 'b'];

interface Bar {
a: string;
b: number;
}

type Baz = { [K in keyof Foo]: Bar[Foo[K]]; };