Skip to content
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

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
110 changes: 84 additions & 26 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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]));
}
}
}
}
@@ -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;
}
@@ -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));
@@ -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;
@@ -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;
@@ -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);
4 changes: 2 additions & 2 deletions src/lib/es2015.symbol.wellknown.d.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}

@@ -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;
};
}

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
Oops, something went wrong.