Skip to content

Commit 06aa905

Browse files
committed
Improve detection and handling of circular generic constraints
1 parent 88c6825 commit 06aa905

File tree

1 file changed

+100
-74
lines changed

1 file changed

+100
-74
lines changed

src/compiler/checker.ts

Lines changed: 100 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ namespace ts {
164164
anyFunctionType.flags |= TypeFlags.ContainsAnyFunctionType;
165165

166166
const noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
167+
const circularConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
167168

168169
const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
169170
const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, unknownType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
@@ -4135,9 +4136,6 @@ namespace ts {
41354136
if (!links.declaredType) {
41364137
const type = <TypeParameter>createType(TypeFlags.TypeParameter);
41374138
type.symbol = symbol;
4138-
if (!(<TypeParameterDeclaration>getDeclarationOfKind(symbol, SyntaxKind.TypeParameter)).constraint) {
4139-
type.constraint = noConstraintType;
4140-
}
41414139
links.declaredType = type;
41424140
}
41434141
return <TypeParameter>links.declaredType;
@@ -4754,19 +4752,79 @@ namespace ts {
47544752
getPropertiesOfObjectType(type);
47554753
}
47564754

4755+
function getConstraintOfTypeVariable(type: TypeVariable): Type {
4756+
return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(<TypeParameter>type) : getBaseConstraintOfTypeVariable(type);
4757+
}
4758+
4759+
function getConstraintOfTypeParameter(typeParameter: TypeParameter): Type {
4760+
return hasNonCircularBaseConstraint(typeParameter) ? getConstraintFromTypeParameter(typeParameter) : undefined;
4761+
}
4762+
4763+
function getBaseConstraintOfTypeVariable(type: TypeVariable): Type {
4764+
const constraint = getResolvedBaseConstraint(type);
4765+
return constraint !== noConstraintType && constraint !== circularConstraintType ? constraint : undefined;
4766+
}
4767+
4768+
function hasNonCircularBaseConstraint(type: TypeVariable): boolean {
4769+
return getResolvedBaseConstraint(type) !== circularConstraintType;
4770+
}
4771+
47574772
/**
4758-
* The apparent type of a type parameter is the base constraint instantiated with the type parameter
4759-
* as the type argument for the 'this' type.
4773+
* Return the resolved base constraint of a type variable. The noConstraintType singleton is returned if the
4774+
* type variable has no constraint, and the circularConstraintType singleton is returned if the constraint
4775+
* circularly references the type variable.
47604776
*/
4761-
function getApparentTypeOfTypeVariable(type: TypeVariable) {
4777+
function getResolvedBaseConstraint(type: TypeVariable): Type {
4778+
let typeStack: Type[];
4779+
let circular: boolean;
47624780
if (!type.resolvedApparentType) {
4763-
let constraintType = getConstraintOfTypeVariable(type);
4764-
while (constraintType && constraintType.flags & TypeFlags.TypeParameter) {
4765-
constraintType = getConstraintOfTypeVariable(<TypeVariable>constraintType);
4766-
}
4767-
type.resolvedApparentType = getTypeWithThisArgument(constraintType || emptyObjectType, type);
4781+
typeStack = [];
4782+
const constraint = getBaseConstraint(type);
4783+
type.resolvedApparentType = circular ? circularConstraintType : getTypeWithThisArgument(constraint || noConstraintType, type);
47684784
}
47694785
return type.resolvedApparentType;
4786+
4787+
function getBaseConstraint(t: Type): Type {
4788+
if (contains(typeStack, t)) {
4789+
circular = true;
4790+
return undefined;
4791+
}
4792+
typeStack.push(t);
4793+
const result = computeBaseConstraint(t);
4794+
typeStack.pop();
4795+
return result;
4796+
}
4797+
4798+
function computeBaseConstraint(t: Type): Type {
4799+
if (t.flags & TypeFlags.TypeParameter) {
4800+
const constraint = getConstraintFromTypeParameter(<TypeParameter>t);
4801+
return (<TypeParameter>t).isThisType ? constraint :
4802+
constraint ? getBaseConstraint(constraint) : undefined;
4803+
}
4804+
if (t.flags & TypeFlags.UnionOrIntersection) {
4805+
const types = (<UnionOrIntersectionType>t).types;
4806+
const baseTypes: Type[] = [];
4807+
for (const type of types) {
4808+
const baseType = getBaseConstraint(type);
4809+
if (baseType) {
4810+
baseTypes.push(baseType);
4811+
}
4812+
}
4813+
return t.flags & TypeFlags.Union && baseTypes.length === types.length ? getUnionType(baseTypes) :
4814+
t.flags & TypeFlags.Intersection && baseTypes.length ? getIntersectionType(baseTypes) :
4815+
undefined;
4816+
}
4817+
if (t.flags & TypeFlags.Index) {
4818+
return stringType;
4819+
}
4820+
if (t.flags & TypeFlags.IndexedAccess) {
4821+
const baseObjectType = getBaseConstraint((<IndexedAccessType>t).objectType);
4822+
const baseIndexType = getBaseConstraint((<IndexedAccessType>t).indexType);
4823+
const baseIndexedAccess = baseObjectType && baseIndexType ? getIndexedAccessType(baseObjectType, baseIndexType) : undefined;
4824+
return baseIndexedAccess && baseIndexedAccess !== unknownType ? getBaseConstraint(baseIndexedAccess) : undefined;
4825+
}
4826+
return t;
4827+
}
47704828
}
47714829

47724830
/**
@@ -4775,7 +4833,7 @@ namespace ts {
47754833
* type itself. Note that the apparent type of a union type is the union type itself.
47764834
*/
47774835
function getApparentType(type: Type): Type {
4778-
const t = type.flags & TypeFlags.TypeVariable ? getApparentTypeOfTypeVariable(<TypeVariable>type) : type;
4836+
const t = type.flags & TypeFlags.TypeVariable ? getBaseConstraintOfTypeVariable(<TypeVariable>type) || emptyObjectType : type;
47794837
return t.flags & TypeFlags.StringLike ? globalStringType :
47804838
t.flags & TypeFlags.NumberLike ? globalNumberType :
47814839
t.flags & TypeFlags.BooleanLike ? globalBooleanType :
@@ -5329,44 +5387,20 @@ namespace ts {
53295387
return (<TypeParameterDeclaration>getDeclarationOfKind(type.symbol, SyntaxKind.TypeParameter)).constraint;
53305388
}
53315389

5332-
function hasConstraintReferenceTo(type: Type, target: TypeParameter): boolean {
5333-
let checked: Type[];
5334-
while (type && type.flags & TypeFlags.TypeParameter && !((type as TypeParameter).isThisType) && !contains(checked, type)) {
5335-
if (type === target) {
5336-
return true;
5337-
}
5338-
(checked || (checked = [])).push(type);
5339-
const constraintDeclaration = getConstraintDeclaration(<TypeParameter>type);
5340-
type = constraintDeclaration && getTypeFromTypeNode(constraintDeclaration);
5341-
}
5342-
return false;
5343-
}
5344-
5345-
function getConstraintOfTypeParameter(typeParameter: TypeParameter): Type {
5390+
function getConstraintFromTypeParameter(typeParameter: TypeParameter): Type {
53465391
if (!typeParameter.constraint) {
53475392
if (typeParameter.target) {
53485393
const targetConstraint = getConstraintOfTypeParameter(typeParameter.target);
53495394
typeParameter.constraint = targetConstraint ? instantiateType(targetConstraint, typeParameter.mapper) : noConstraintType;
53505395
}
53515396
else {
53525397
const constraintDeclaration = getConstraintDeclaration(typeParameter);
5353-
let constraint = getTypeFromTypeNode(constraintDeclaration);
5354-
if (hasConstraintReferenceTo(constraint, typeParameter)) {
5355-
error(constraintDeclaration, Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(typeParameter));
5356-
constraint = unknownType;
5357-
}
5358-
typeParameter.constraint = constraint;
5398+
typeParameter.constraint = constraintDeclaration ? getTypeFromTypeNode(constraintDeclaration) : noConstraintType;
53595399
}
53605400
}
53615401
return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint;
53625402
}
53635403

5364-
function getConstraintOfTypeVariable(type: TypeVariable): Type {
5365-
return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(<TypeParameter>type) :
5366-
type.flags & TypeFlags.IndexedAccess ? (<IndexedAccessType>type).constraint :
5367-
undefined;
5368-
}
5369-
53705404
function getParentSymbolOfTypeParameter(typeParameter: TypeParameter): Symbol {
53715405
return getSymbolOfNode(getDeclarationOfKind(typeParameter.symbol, SyntaxKind.TypeParameter).parent);
53725406
}
@@ -6042,24 +6076,6 @@ namespace ts {
60426076
const type = <IndexedAccessType>createType(TypeFlags.IndexedAccess);
60436077
type.objectType = objectType;
60446078
type.indexType = indexType;
6045-
// We eagerly compute the constraint of the indexed access type such that circularity
6046-
// errors are immediately caught and reported. For example, class C { x: this["x"] }
6047-
// becomes an error only when the constraint is eagerly computed.
6048-
if (type.objectType.flags & TypeFlags.StructuredType) {
6049-
// The constraint of T[K], where T is an object, union, or intersection type,
6050-
// is the type of the string index signature of T, if any.
6051-
type.constraint = getIndexTypeOfType(type.objectType, IndexKind.String);
6052-
}
6053-
else if (type.objectType.flags & TypeFlags.TypeVariable) {
6054-
// The constraint of T[K], where T is a type variable, is A[K], where A is the
6055-
// apparent type of T.
6056-
const apparentType = getApparentTypeOfTypeVariable(<TypeVariable>type.objectType);
6057-
if (apparentType !== emptyObjectType) {
6058-
type.constraint = isTypeOfKind((<IndexedAccessType>type).indexType, TypeFlags.StringLike) ?
6059-
getIndexedAccessType(apparentType, (<IndexedAccessType>type).indexType) :
6060-
getIndexTypeOfType(apparentType, IndexKind.String);
6061-
}
6062-
}
60636079
return type;
60646080
}
60656081

@@ -6150,13 +6166,6 @@ namespace ts {
61506166
if (objectType.flags & TypeFlags.Any) {
61516167
return objectType;
61526168
}
6153-
// We first check that the index type is assignable to 'keyof T' for the object type.
6154-
if (accessNode) {
6155-
if (!isTypeAssignableTo(indexType, getIndexType(objectType))) {
6156-
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType));
6157-
return unknownType;
6158-
}
6159-
}
61606169
// If the object type is a mapped type { [P in K]: E }, we instantiate E using a mapper that substitutes
61616170
// the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the
61626171
// type Box<T[X]>.
@@ -7436,8 +7445,9 @@ namespace ts {
74367445
}
74377446
// A type S is related to a type T[K] if S is related to A[K], where K is string-like and
74387447
// A is the apparent type of S.
7439-
if ((<IndexedAccessType>target).constraint) {
7440-
if (result = isRelatedTo(source, (<IndexedAccessType>target).constraint, reportErrors)) {
7448+
const constraint = getBaseConstraintOfTypeVariable(<IndexedAccessType>target);
7449+
if (constraint) {
7450+
if (result = isRelatedTo(source, constraint, reportErrors)) {
74417451
errorInfo = saveErrorInfo;
74427452
return result;
74437453
}
@@ -7475,8 +7485,9 @@ namespace ts {
74757485
else if (source.flags & TypeFlags.IndexedAccess) {
74767486
// A type S[K] is related to a type T if A[K] is related to T, where K is string-like and
74777487
// A is the apparent type of S.
7478-
if ((<IndexedAccessType>source).constraint) {
7479-
if (result = isRelatedTo((<IndexedAccessType>source).constraint, target, reportErrors)) {
7488+
const constraint = getBaseConstraintOfTypeVariable(<IndexedAccessType>source);
7489+
if (constraint) {
7490+
if (result = isRelatedTo(constraint, target, reportErrors)) {
74807491
errorInfo = saveErrorInfo;
74817492
return result;
74827493
}
@@ -12528,7 +12539,7 @@ namespace ts {
1252812539
return unknownType;
1252912540
}
1253012541

12531-
return getIndexedAccessType(objectType, indexType, node);
12542+
return checkIndexedAccessIndexType(getIndexedAccessType(objectType, indexType, node), node);
1253212543
}
1253312544

1253412545
function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: Type, reportError: boolean): boolean {
@@ -15186,14 +15197,14 @@ namespace ts {
1518615197
function isLiteralContextualType(contextualType: Type) {
1518715198
if (contextualType) {
1518815199
if (contextualType.flags & TypeFlags.TypeVariable) {
15189-
const apparentType = getApparentTypeOfTypeVariable(<TypeVariable>contextualType);
15200+
const constraint = getBaseConstraintOfTypeVariable(<TypeVariable>contextualType) || emptyObjectType;
1519015201
// If the type parameter is constrained to the base primitive type we're checking for,
1519115202
// consider this a literal context. For example, given a type parameter 'T extends string',
1519215203
// this causes us to infer string literal types for T.
15193-
if (apparentType.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean | TypeFlags.Enum)) {
15204+
if (constraint.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean | TypeFlags.Enum)) {
1519415205
return true;
1519515206
}
15196-
contextualType = apparentType;
15207+
contextualType = constraint;
1519715208
}
1519815209
return maybeTypeOfKind(contextualType, (TypeFlags.Literal | TypeFlags.Index));
1519915210
}
@@ -15391,6 +15402,10 @@ namespace ts {
1539115402
}
1539215403

1539315404
checkSourceElement(node.constraint);
15405+
const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node));
15406+
if (!hasNonCircularBaseConstraint(typeParameter)) {
15407+
error(node.constraint, Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(typeParameter));
15408+
}
1539415409
getConstraintOfTypeParameter(getDeclaredTypeOfTypeParameter(getSymbolOfNode(node)));
1539515410
if (produceDiagnostics) {
1539615411
checkTypeNameIsReserved(node.name, Diagnostics.Type_parameter_name_cannot_be_0);
@@ -16014,17 +16029,28 @@ namespace ts {
1601416029
forEach(node.types, checkSourceElement);
1601516030
}
1601616031

16032+
function checkIndexedAccessIndexType(type: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode) {
16033+
if (type.flags & TypeFlags.IndexedAccess) {
16034+
// Check that the index type is assignable to 'keyof T' for the object type.
16035+
const objectType = (<IndexedAccessType>type).objectType;
16036+
const indexType = (<IndexedAccessType>type).indexType;
16037+
if (!isTypeAssignableTo(indexType, getIndexType(objectType))) {
16038+
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType));
16039+
}
16040+
}
16041+
return type;
16042+
}
16043+
1601716044
function checkIndexedAccessType(node: IndexedAccessTypeNode) {
16018-
getTypeFromIndexedAccessTypeNode(node);
16045+
checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node);
1601916046
}
1602016047

1602116048
function checkMappedType(node: MappedTypeNode) {
1602216049
checkSourceElement(node.typeParameter);
1602316050
checkSourceElement(node.type);
1602416051
const type = <MappedType>getTypeFromMappedTypeNode(node);
1602516052
const constraintType = getConstraintTypeFromMappedType(type);
16026-
const keyType = constraintType.flags & TypeFlags.TypeVariable ? getApparentTypeOfTypeVariable(<TypeVariable>constraintType) : constraintType;
16027-
checkTypeAssignableTo(keyType, stringType, node.typeParameter.constraint);
16053+
checkTypeAssignableTo(constraintType, stringType, node.typeParameter.constraint);
1602816054
}
1602916055

1603016056
function isPrivateWithinAmbient(node: Node): boolean {

0 commit comments

Comments
 (0)