Skip to content
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]]; };