Skip to content

Commit

Permalink
Merge pull request #12351 from Microsoft/mappedTypeRelations
Browse files Browse the repository at this point in the history
Higher order type relations for mapped types
  • Loading branch information
ahejlsberg authored Nov 18, 2016
2 parents 9945529 + a789990 commit e81da9c
Show file tree
Hide file tree
Showing 7 changed files with 632 additions and 33 deletions.
126 changes: 97 additions & 29 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ namespace ts {
const intersectionTypes = createMap<IntersectionType>();
const stringLiteralTypes = createMap<LiteralType>();
const numericLiteralTypes = createMap<LiteralType>();
const indexedAccessTypes = createMap<IndexedAccessType>();
const evolvingArrayTypes: EvolvingArrayType[] = [];

const unknownSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, "unknown");
Expand Down Expand Up @@ -4665,13 +4666,22 @@ namespace ts {
return type.resolvedApparentType;
}

/**
* The apparent type of an indexed access T[K] is the type of T's string index signature, if any.
*/
function getApparentTypeOfIndexedAccess(type: IndexedAccessType) {
return getIndexTypeOfType(getApparentType(type.objectType), IndexKind.String) || type;
}

/**
* For a type parameter, return the base constraint of the type parameter. For the string, number,
* boolean, and symbol primitive types, return the corresponding object types. Otherwise return the
* type itself. Note that the apparent type of a union type is the union type itself.
*/
function getApparentType(type: Type): Type {
const t = type.flags & TypeFlags.TypeParameter ? getApparentTypeOfTypeParameter(<TypeParameter>type) : type;
const t = type.flags & TypeFlags.TypeParameter ? getApparentTypeOfTypeParameter(<TypeParameter>type) :
type.flags & TypeFlags.IndexedAccess ? getApparentTypeOfIndexedAccess(<IndexedAccessType>type) :
type;
return t.flags & TypeFlags.StringLike ? globalStringType :
t.flags & TypeFlags.NumberLike ? globalNumberType :
t.flags & TypeFlags.BooleanLike ? globalBooleanType :
Expand Down Expand Up @@ -5907,6 +5917,7 @@ namespace ts {

function getIndexType(type: Type): Type {
return type.flags & TypeFlags.TypeParameter ? getIndexTypeForTypeParameter(<TypeParameter>type) :
getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(<MappedType>type) :
type.flags & TypeFlags.Any || getIndexInfoOfType(type, IndexKind.String) ? stringOrNumberType :
getIndexInfoOfType(type, IndexKind.Number) ? getUnionType([numberType, getLiteralTypeFromPropertyNames(type)]) :
getLiteralTypeFromPropertyNames(type);
Expand All @@ -5920,18 +5931,13 @@ namespace ts {
return links.resolvedType;
}

function createIndexedAccessType(objectType: Type, indexType: TypeParameter) {
function createIndexedAccessType(objectType: Type, indexType: Type) {
const type = <IndexedAccessType>createType(TypeFlags.IndexedAccess);
type.objectType = objectType;
type.indexType = indexType;
return type;
}

function getIndexedAccessTypeForTypeParameter(objectType: Type, indexType: TypeParameter) {
const indexedAccessTypes = indexType.resolvedIndexedAccessTypes || (indexType.resolvedIndexedAccessTypes = []);
return indexedAccessTypes[objectType.id] || (indexedAccessTypes[objectType.id] = createIndexedAccessType(objectType, indexType));
}

function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode, cacheSymbol: boolean) {
const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? <ElementAccessExpression>accessNode : undefined;
const propName = indexType.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral | TypeFlags.EnumLiteral) ?
Expand Down Expand Up @@ -5995,13 +6001,41 @@ namespace ts {
return unknownType;
}

function getIndexedAccessForMappedType(type: MappedType, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? <ElementAccessExpression>accessNode : undefined;
if (accessExpression && isAssignmentTarget(accessExpression) && type.declaration.readonlyToken) {
error(accessExpression, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(type));
return unknownType;
}
const mapper = createUnaryTypeMapper(getTypeParameterFromMappedType(type), indexType);
const templateMapper = type.mapper ? combineTypeMappers(type.mapper, mapper) : mapper;
return addOptionality(instantiateType(getTemplateTypeFromMappedType(type), templateMapper), !!type.declaration.questionToken);
}

function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
if (indexType.flags & TypeFlags.TypeParameter) {
if (accessNode && !isTypeAssignableTo(getConstraintOfTypeParameter(<TypeParameter>indexType) || emptyObjectType, getIndexType(objectType))) {
error(accessNode, Diagnostics.Type_0_is_not_constrained_to_keyof_1, typeToString(indexType), typeToString(objectType));
return unknownType;
if (indexType.flags & TypeFlags.TypeParameter ||
objectType.flags & TypeFlags.TypeParameter && indexType.flags & TypeFlags.Index ||
isGenericMappedType(objectType)) {
// If either the object type or the index type are type parameters, or if the object type is a mapped
// type with a generic constraint, we are performing a higher-order index access where we cannot
// meaningfully access the properties of the object type. In those cases, we first check that the
// index type is assignable to 'keyof T' for the object type.
if (accessNode) {
const keyType = indexType.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(<TypeParameter>indexType) || emptyObjectType : indexType;
if (!isTypeAssignableTo(keyType, getIndexType(objectType))) {
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType));
return unknownType;
}
}
// If the object type is a mapped type { [P in K]: E }, we instantiate E using a mapper that substitutes
// the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the
// type Box<T[X]>.
if (isGenericMappedType(objectType)) {
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
}
return getIndexedAccessTypeForTypeParameter(objectType, <TypeParameter>indexType);
// Otherwise we defer the operation by creating an indexed access type.
const id = objectType.id + "," + indexType.id;
return indexedAccessTypes[id] || (indexedAccessTypes[id] = createIndexedAccessType(objectType, indexType));
}
const apparentType = getApparentType(objectType);
if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Primitive)) {
Expand Down Expand Up @@ -6034,6 +6068,9 @@ namespace ts {
type.aliasSymbol = getAliasSymbolForTypeNode(node);
type.aliasTypeArguments = getAliasTypeArgumentsForTypeNode(node);
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 @@ -7153,12 +7190,24 @@ namespace ts {
}

if (target.flags & TypeFlags.TypeParameter) {
// Given a type parameter K with a constraint keyof T, a type S is
// assignable to K if S is assignable to keyof T.
const constraint = getConstraintOfTypeParameter(<TypeParameter>target);
if (constraint && constraint.flags & TypeFlags.Index) {
if (result = isRelatedTo(source, constraint, reportErrors)) {
return result;
// A source type { [P in keyof T]: X } is related to a target type T if X is related to T[P].
if (getObjectFlags(source) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(<MappedType>source) === getIndexType(target)) {
if (!(<MappedType>source).declaration.questionToken) {
const templateType = getTemplateTypeFromMappedType(<MappedType>source);
const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(<MappedType>source));
if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) {
return result;
}
}
}
else {
// Given a type parameter K with a constraint keyof T, a type S is
// assignable to K if S is assignable to keyof T.
const constraint = getConstraintOfTypeParameter(<TypeParameter>target);
if (constraint && constraint.flags & TypeFlags.Index) {
if (result = isRelatedTo(source, constraint, reportErrors)) {
return result;
}
}
}
}
Expand All @@ -7178,22 +7227,41 @@ namespace ts {
}
}
}
else if (target.flags & TypeFlags.IndexedAccess) {
// if we have indexed access types with identical index types, see if relationship holds for
// the two object types.
if (source.flags & TypeFlags.IndexedAccess && (<IndexedAccessType>source).indexType === (<IndexedAccessType>target).indexType) {
if (result = isRelatedTo((<IndexedAccessType>source).objectType, (<IndexedAccessType>target).objectType, reportErrors)) {
return result;
}
}
}

if (source.flags & TypeFlags.TypeParameter) {
let constraint = getConstraintOfTypeParameter(<TypeParameter>source);

if (!constraint || constraint.flags & TypeFlags.Any) {
constraint = emptyObjectType;
// A source type T is related to a target type { [P in keyof T]: X } if T[P] is related to X.
if (getObjectFlags(target) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(<MappedType>target) === getIndexType(source)) {
const indexedAccessType = getIndexedAccessType(source, getTypeParameterFromMappedType(<MappedType>target));
const templateType = getTemplateTypeFromMappedType(<MappedType>target);
if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) {
return result;
}
}
else {
let constraint = getConstraintOfTypeParameter(<TypeParameter>source);

// The constraint may need to be further instantiated with its 'this' type.
constraint = getTypeWithThisArgument(constraint, source);
if (!constraint || constraint.flags & TypeFlags.Any) {
constraint = emptyObjectType;
}

// Report constraint errors only if the constraint is not the empty object type
const reportConstraintErrors = reportErrors && constraint !== emptyObjectType;
if (result = isRelatedTo(constraint, target, reportConstraintErrors)) {
errorInfo = saveErrorInfo;
return result;
// The constraint may need to be further instantiated with its 'this' type.
constraint = getTypeWithThisArgument(constraint, source);

// Report constraint errors only if the constraint is not the empty object type
const reportConstraintErrors = reportErrors && constraint !== emptyObjectType;
if (result = isRelatedTo(constraint, target, reportConstraintErrors)) {
errorInfo = saveErrorInfo;
return result;
}
}
}
else {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1731,7 +1731,7 @@
"category": "Error",
"code": 2535
},
"Type '{0}' is not constrained to 'keyof {1}'.": {
"Type '{0}' cannot be used to index type '{1}'.": {
"category": "Error",
"code": 2536
},
Expand Down
4 changes: 1 addition & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2974,8 +2974,6 @@ namespace ts {
/* @internal */
resolvedIndexType: IndexType;
/* @internal */
resolvedIndexedAccessTypes: IndexedAccessType[];
/* @internal */
isThisType?: boolean;
}

Expand All @@ -2985,7 +2983,7 @@ namespace ts {

export interface IndexedAccessType extends Type {
objectType: Type;
indexType: TypeParameter;
indexType: Type;
}

export const enum SignatureKind {
Expand Down
Loading

0 comments on commit e81da9c

Please sign in to comment.