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
49 changes: 41 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13340,6 +13340,18 @@ namespace ts {
return inferences && getIntersectionType(inferences);
}

function getTupleConstraintFromMappedTypeNode(node: MappedTypeNode) {
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)) {
return keyOfTarget;
}
}
}

/** This is a worker function. Use getConstraintOfTypeParameter which guards against circular constraints. */
function getConstraintFromTypeParameter(typeParameter: TypeParameter): Type | undefined {
if (!typeParameter.constraint) {
Expand All @@ -13353,6 +13365,14 @@ 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 keyOfTarget = getTupleConstraintFromMappedTypeNode(constraintDeclaration.parent.parent as MappedTypeNode);
if (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 +15868,16 @@ 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);
// 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
const keyOfTarget = getTupleConstraintFromMappedTypeNode(node);
if (keyOfTarget) {
// Instantiate the mapped type over a tuple with an identity mapper
links.resolvedType = instantiateMappedTupleType(
keyOfTarget,
type,
makeFunctionTypeMapper(identity)
);
}
}
return links.resolvedType;
}
Expand Down Expand Up @@ -35579,14 +35609,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
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 ShouldErrorOnInterpolation = {
[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 ShouldErrorOnInterpolation = {
[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 ShouldErrorOnInterpolation = {
>ShouldErrorOnInterpolation : Symbol(ShouldErrorOnInterpolation, 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 ShouldErrorOnInterpolation = {
>ShouldErrorOnInterpolation : ["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 ShouldErrorOnInterpolation = {
[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]]; };