Skip to content
110 changes: 84 additions & 26 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13720,10 +13720,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type.modifiersType;
}

function getMappedTypeNodeModifiers(node: MappedTypeNode) {
return (node.readonlyToken ? node.readonlyToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) |
(node.questionToken ? node.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0);
}

function getMappedTypeModifiers(type: MappedType): MappedTypeModifiers {
const declaration = type.declaration;
return (declaration.readonlyToken ? declaration.readonlyToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) |
(declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0);
return getMappedTypeNodeModifiers(type.declaration);
}

function getMappedTypeOptionality(type: MappedType): number {
Expand Down Expand Up @@ -15881,14 +15884,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// Given a homomorphic mapped type { [K in keyof T]: XXX }, where T is constrained to an array or tuple type, in the
// template type XXX, K has an added constraint of number | `${number}`.
else if (type.flags & TypeFlags.TypeParameter && parent.kind === SyntaxKind.MappedType && node === (parent as MappedTypeNode).type) {
const mappedType = getTypeFromTypeNode(parent as TypeNode) as MappedType;
if (getTypeParameterFromMappedType(mappedType) === getActualTypeVariable(type)) {
const typeParameter = getHomomorphicTypeVariable(mappedType);
if (typeParameter) {
const constraint = getConstraintOfTypeParameter(typeParameter);
if (constraint && everyType(constraint, isArrayOrTupleType)) {
constraints = append(constraints, getUnionType([numberType, numericStringType]));
else if (type.flags & TypeFlags.TypeParameter && parent.kind === SyntaxKind.MappedType && node === (parent as MappedTypeNode).type && !(parent as MappedTypeNode).nameType) {
const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfDeclaration((parent as MappedTypeNode).typeParameter));
const constraintType = getConstraintOfTypeParameter(typeParameter) || errorType;
const arrayOrTuple = getArrayOrTupleOriginIndexType(constraintType);
if (arrayOrTuple) {
if (isTupleType(arrayOrTuple)) {
constraints = append(constraints, getUnionType(map(getTypeArguments(arrayOrTuple), (_, i) => getStringLiteralType("" + i))));
}
else {
constraints = append(constraints, getUnionType([numberType, numericStringType]));
}
}
else {
if (typeParameter === getActualTypeVariable(type)) {
const typeVariable = constraintType.flags & TypeFlags.Index && getActualTypeVariable((constraintType as IndexType).type);
const homomorphicTypeVariable = typeVariable && typeVariable.flags & TypeFlags.TypeParameter ? typeVariable as TypeParameter : undefined;
if (homomorphicTypeVariable) {
const constraint = getConstraintOfTypeParameter(homomorphicTypeVariable);
if (constraint && everyType(constraint, isArrayOrTupleType)) {
constraints = append(constraints, getUnionType([numberType, numericStringType]));
}
}
}
}
Expand Down Expand Up @@ -18152,17 +18168,58 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return links.resolvedType;
}

function getArrayOrTupleOriginIndexType(type: Type) {
if (!(type.flags & TypeFlags.Union)) {
return;
}
const origin = (type as UnionType).origin;
if (!origin || !(origin.flags & TypeFlags.Index)) {
return;
}
const originType = (origin as IndexType).type;
return isArrayOrTupleType(originType) ? originType : undefined;
}

function getTypeFromMappedTypeNode(node: MappedTypeNode): Type {
const links = getNodeLinks(node);
if (!links.resolvedType) {
// Eagerly resolve the constraint type which forces an error if the constraint type circularly
// references itself through one or more type aliases.
const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfDeclaration(node.typeParameter));
const constraintType = getConstraintOfTypeParameter(typeParameter) || errorType;
const arrayOrTuple = !node.nameType && getArrayOrTupleOriginIndexType(constraintType);
if (arrayOrTuple) {
if (!node.type) {
return errorType;
}
const modifiers = getMappedTypeNodeModifiers(node);
const templateType = addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true, !!(modifiers & MappedTypeModifiers.IncludeOptional));

if (isTupleType(arrayOrTuple)) {
return links.resolvedType = instantiateMappedTupleType(
arrayOrTuple,
modifiers,
typeParameter,
templateType,
/*mapper*/ undefined,
);
}

return links.resolvedType = instantiateMappedArrayType(
arrayOrTuple,
modifiers,
typeParameter,
templateType,
/*mapper*/ undefined,
);
}
const type = createObjectType(ObjectFlags.Mapped, node.symbol) as MappedType;
type.declaration = node;
type.aliasSymbol = getAliasSymbolForTypeNode(node);
type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(type.aliasSymbol);
type.typeParameter = typeParameter;
type.constraintType = constraintType;
links.resolvedType = type;
// Eagerly resolve the constraint type which forces an error if the constraint type circularly
// references itself through one or more type aliases.
getConstraintTypeFromMappedType(type);
}
return links.resolvedType;
}
Expand Down Expand Up @@ -19310,13 +19367,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
isArrayType(t) || t.flags & TypeFlags.Any && findResolutionCycleStartIndex(typeVariable, TypeSystemPropertyName.ImmediateBaseConstraint) < 0 &&
(constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, isArrayOrTupleType)
) {
return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable, t, mapper));
return instantiateMappedArrayType(t, getMappedTypeModifiers(type), getTypeParameterFromMappedType(type), getTemplateTypeFromMappedType(type.target as MappedType || type), prependTypeMapping(typeVariable, t, mapper));
}
if (isGenericTupleType(t)) {
return instantiateMappedGenericTupleType(t, type, typeVariable, mapper);
}
if (isTupleType(t)) {
return instantiateMappedTupleType(t, type, prependTypeMapping(typeVariable, t, mapper));
return instantiateMappedTupleType(t, getMappedTypeModifiers(type), getTypeParameterFromMappedType(type), getTemplateTypeFromMappedType(type.target as MappedType || type), prependTypeMapping(typeVariable, t, mapper));
}
}
return instantiateAnonymousType(type, prependTypeMapping(typeVariable, t, mapper));
Expand Down Expand Up @@ -19358,16 +19415,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return createTupleType(elementTypes, map(elementTypes, _ => ElementFlags.Variadic), newReadonly);
}

function instantiateMappedArrayType(arrayType: Type, mappedType: MappedType, mapper: TypeMapper) {
const elementType = instantiateMappedTypeTemplate(mappedType, numberType, /*isOptional*/ true, mapper);
function instantiateMappedArrayType(arrayType: Type, modifiers: MappedTypeModifiers, typeParameter: TypeParameter, templateType: Type, mapper: TypeMapper | undefined) {
const elementType = instantiateMappedTypeTemplate(modifiers, typeParameter, templateType, numberType, /*isOptional*/ true, mapper);
return isErrorType(elementType) ? errorType :
createArrayType(elementType, getModifiedReadonlyState(isReadonlyArrayType(arrayType), getMappedTypeModifiers(mappedType)));
createArrayType(elementType, getModifiedReadonlyState(isReadonlyArrayType(arrayType), modifiers));
}

function instantiateMappedTupleType(tupleType: TupleTypeReference, mappedType: MappedType, mapper: TypeMapper) {
function instantiateMappedTupleType(tupleType: TupleTypeReference, modifiers: MappedTypeModifiers, typeParameter: TypeParameter, templateType: Type, mapper: TypeMapper | undefined) {
const elementFlags = tupleType.target.elementFlags;
const elementTypes = map(getElementTypes(tupleType), (_, i) => instantiateMappedTypeTemplate(mappedType, getStringLiteralType("" + i), !!(elementFlags[i] & ElementFlags.Optional), mapper));
const modifiers = getMappedTypeModifiers(mappedType);
const elementTypes = map(getElementTypes(tupleType), (_, i) => instantiateMappedTypeTemplate(modifiers, typeParameter, templateType, getStringLiteralType("" + i), !!(elementFlags[i] & ElementFlags.Optional), mapper));
const newTupleModifiers = modifiers & MappedTypeModifiers.IncludeOptional ? map(elementFlags, f => f & ElementFlags.Required ? ElementFlags.Optional : f) :
modifiers & MappedTypeModifiers.ExcludeOptional ? map(elementFlags, f => f & ElementFlags.Optional ? ElementFlags.Required : f) :
elementFlags;
Expand All @@ -19376,10 +19432,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
createTupleType(elementTypes, newTupleModifiers, newReadonly, tupleType.target.labeledElementDeclarations);
}

function instantiateMappedTypeTemplate(type: MappedType, key: Type, isOptional: boolean, mapper: TypeMapper) {
const templateMapper = appendTypeMapping(mapper, getTypeParameterFromMappedType(type), key);
const propType = instantiateType(getTemplateTypeFromMappedType(type.target as MappedType || type), templateMapper);
const modifiers = getMappedTypeModifiers(type);
function instantiateMappedTypeTemplate(modifiers: MappedTypeModifiers, typeParameter: TypeParameter, templateType: Type, key: Type, isOptional: boolean, mapper: TypeMapper | undefined) {
const templateMapper = appendTypeMapping(mapper, typeParameter, key);
const propType = instantiateType(templateType, templateMapper);
return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType, /*isProperty*/ true) :
strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) :
propType;
Expand Down Expand Up @@ -39616,6 +39671,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

const type = getTypeFromMappedTypeNode(node) as MappedType;
if (!(getObjectFlags(type) & ObjectFlags.Mapped)) {
return;
}
const nameType = getNameTypeFromMappedType(type);
if (nameType) {
checkTypeAssignableTo(nameType, keyofConstraintType, node.nameType);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/es2015.symbol.wellknown.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ interface Array<T> {
* when they will be absent when used in a 'with' statement.
*/
readonly [Symbol.unscopables]: {
[K in keyof any[]]?: boolean;
[K in keyof any[] as K]?: boolean;
};
}

Expand All @@ -87,7 +87,7 @@ interface ReadonlyArray<T> {
* when they will be absent when used in a 'with' statement.
*/
readonly [Symbol.unscopables]: {
[K in keyof readonly any[]]?: boolean;
[K in keyof readonly any[] as K]?: boolean;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
mappedTypeConcreteTupleHomomorphism.ts(27,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'.


==== mappedTypeConcreteTupleHomomorphism.ts (1 errors) ====
type TupleOfNumbers = [1, 2]

type HomomorphicType = {
[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
}
const homomorphic: HomomorphicType = ['1', '2']

type TupleOfNumbersKeys = keyof TupleOfNumbers
type HomomorphicType2 = {
[K in TupleOfNumbersKeys]: `${TupleOfNumbers[K]}`
}
const homomorphic2: HomomorphicType2 = ['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]]; };

47 changes: 47 additions & 0 deletions tests/baselines/reference/mappedTypeConcreteTupleHomomorphism.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//// [tests/cases/compiler/mappedTypeConcreteTupleHomomorphism.ts] ////

//// [mappedTypeConcreteTupleHomomorphism.ts]
type TupleOfNumbers = [1, 2]

type HomomorphicType = {
[K in keyof TupleOfNumbers]: `${TupleOfNumbers[K]}`
}
const homomorphic: HomomorphicType = ['1', '2']

type TupleOfNumbersKeys = keyof TupleOfNumbers
type HomomorphicType2 = {
[K in TupleOfNumbersKeys]: `${TupleOfNumbers[K]}`
}
const homomorphic2: HomomorphicType2 = ['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 homomorphic2 = ['1', '2'];
var d = [1, 1, 1];
Loading