Skip to content

Commit a11c416

Browse files
authoredSep 19, 2022
Improve checking of in operator (#50666)
* Improve checking of `in` operator * Accept new baselines * Add tests * Delete old and accept new baselines * Disallow right operand of type '{}' * Accept new baselines * Support number and symbol literals * Add tests * Disallow {} typed right operand only in strictNullChecks mode * Accept new baselines * Detect {} resulting from intersections * Accept new baselines * Don't attempt to reduce intersections with Record<K, unknown> * Accept new baselines * Return undefined instead of unknownSymbol from getGlobalRecordSymbol()
1 parent 67f2b62 commit a11c416

31 files changed

+5090
-936
lines changed
 

‎src/compiler/checker.ts

+44-49
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,8 @@ namespace ts {
858858
emptyTypeLiteralSymbol.members = createSymbolTable();
859859
const emptyTypeLiteralType = createAnonymousType(emptyTypeLiteralSymbol, emptySymbols, emptyArray, emptyArray, emptyArray);
860860

861-
const unknownUnionType = strictNullChecks ? getUnionType([undefinedType, nullType, createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray)]) : unknownType;
861+
const unknownEmptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray);
862+
const unknownUnionType = strictNullChecks ? getUnionType([undefinedType, nullType, unknownEmptyObjectType]) : unknownType;
862863

863864
const emptyGenericType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray) as ObjectType as GenericType;
864865
emptyGenericType.instantiations = new Map<string, TypeReference>();
@@ -998,6 +999,7 @@ namespace ts {
998999
let deferredGlobalOmitSymbol: Symbol | undefined;
9991000
let deferredGlobalAwaitedSymbol: Symbol | undefined;
10001001
let deferredGlobalBigIntType: ObjectType | undefined;
1002+
let deferredGlobalRecordSymbol: Symbol | undefined;
10011003

10021004
const allPotentiallyUnusedIdentifiers = new Map<Path, PotentiallyUnusedIdentifier[]>(); // key is file name
10031005

@@ -14314,6 +14316,11 @@ namespace ts {
1431414316
return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as __String, /*arity*/ 0, /*reportErrors*/ false)) || emptyObjectType;
1431514317
}
1431614318

14319+
function getGlobalRecordSymbol(): Symbol | undefined {
14320+
deferredGlobalRecordSymbol ||= getGlobalTypeAliasSymbol("Record" as __String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol;
14321+
return deferredGlobalRecordSymbol === unknownSymbol ? undefined : deferredGlobalRecordSymbol;
14322+
}
14323+
1431714324
/**
1431814325
* Instantiates a global type that is generic with some element type, and returns that instantiation.
1431914326
*/
@@ -25199,19 +25206,27 @@ namespace ts {
2519925206

2520025207
function isTypePresencePossible(type: Type, propName: __String, assumeTrue: boolean) {
2520125208
const prop = getPropertyOfType(type, propName);
25202-
if (prop) {
25203-
return prop.flags & SymbolFlags.Optional ? true : assumeTrue;
25204-
}
25205-
return getApplicableIndexInfoForName(type, propName) ? true : !assumeTrue;
25209+
return prop ?
25210+
!!(prop.flags & SymbolFlags.Optional) || assumeTrue :
25211+
!!getApplicableIndexInfoForName(type, propName) || !assumeTrue;
2520625212
}
2520725213

25208-
function narrowByInKeyword(type: Type, name: __String, assumeTrue: boolean) {
25209-
if (type.flags & TypeFlags.Union
25210-
|| type.flags & TypeFlags.Object && declaredType !== type && !(declaredType === unknownType && isEmptyAnonymousObjectType(type))
25211-
|| isThisTypeParameter(type)
25212-
|| type.flags & TypeFlags.Intersection && every((type as IntersectionType).types, t => t.symbol !== globalThisSymbol)) {
25214+
function narrowByInKeyword(type: Type, nameType: StringLiteralType | NumberLiteralType | UniqueESSymbolType, assumeTrue: boolean) {
25215+
const name = getPropertyNameFromType(nameType);
25216+
const isKnownProperty = someType(type, t => isTypePresencePossible(t, name, /*assumeTrue*/ true));
25217+
if (isKnownProperty) {
25218+
// If the check is for a known property (i.e. a property declared in some constituent of
25219+
// the target type), we filter the target type by presence of absence of the property.
2521325220
return filterType(type, t => isTypePresencePossible(t, name, assumeTrue));
2521425221
}
25222+
if (assumeTrue) {
25223+
// If the check is for an unknown property, we intersect the target type with `Record<X, unknown>`,
25224+
// where X is the name of the property.
25225+
const recordSymbol = getGlobalRecordSymbol();
25226+
if (recordSymbol) {
25227+
return getIntersectionType([type, getTypeAliasInstantiation(recordSymbol, [nameType, unknownType])]);
25228+
}
25229+
}
2521525230
return type;
2521625231
}
2521725232

@@ -25271,15 +25286,14 @@ namespace ts {
2527125286
return narrowTypeByPrivateIdentifierInInExpression(type, expr, assumeTrue);
2527225287
}
2527325288
const target = getReferenceCandidate(expr.right);
25274-
const leftType = getTypeOfNode(expr.left);
25275-
if (leftType.flags & TypeFlags.StringLiteral) {
25276-
const name = escapeLeadingUnderscores((leftType as StringLiteralType).value);
25289+
const leftType = getTypeOfExpression(expr.left);
25290+
if (leftType.flags & TypeFlags.StringOrNumberLiteralOrUnique) {
2527725291
if (containsMissingType(type) && isAccessExpression(reference) && isMatchingReference(reference.expression, target) &&
25278-
getAccessedPropertyName(reference) === name) {
25292+
getAccessedPropertyName(reference) === getPropertyNameFromType(leftType as StringLiteralType | NumberLiteralType | UniqueESSymbolType)) {
2527925293
return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined);
2528025294
}
2528125295
if (isMatchingReference(reference, target)) {
25282-
return narrowByInKeyword(type, name, assumeTrue);
25296+
return narrowByInKeyword(type, leftType as StringLiteralType | NumberLiteralType | UniqueESSymbolType, assumeTrue);
2528325297
}
2528425298
}
2528525299
break;
@@ -33848,6 +33862,10 @@ namespace ts {
3384833862
return booleanType;
3384933863
}
3385033864

33865+
function hasEmptyObjectIntersection(type: Type): boolean {
33866+
return someType(type, t => t === unknownEmptyObjectType || !!(t.flags & TypeFlags.Intersection) && some((t as IntersectionType).types, isEmptyAnonymousObjectType));
33867+
}
33868+
3385133869
function checkInExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type {
3385233870
if (leftType === silentNeverType || rightType === silentNeverType) {
3385333871
return silentNeverType;
@@ -33864,43 +33882,20 @@ namespace ts {
3386433882
}
3386533883
}
3386633884
else {
33867-
leftType = checkNonNullType(leftType, left);
33868-
// TypeScript 1.0 spec (April 2014): 4.15.5
33869-
// Require the left operand to be of type Any, the String primitive type, or the Number primitive type.
33870-
if (!(allTypesAssignableToKind(leftType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) ||
33871-
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) {
33872-
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_a_private_identifier_or_of_type_any_string_number_or_symbol);
33885+
// The type of the lef operand must be assignable to string, number, or symbol.
33886+
checkTypeAssignableTo(checkNonNullType(leftType, left), stringNumberSymbolType, left);
33887+
}
33888+
// The type of the right operand must be assignable to 'object'.
33889+
if (checkTypeAssignableTo(checkNonNullType(rightType, right), nonPrimitiveType, right)) {
33890+
// The {} type is assignable to the object type, yet {} might represent a primitive type. Here we
33891+
// detect and error on {} that results from narrowing the unknown type, as well as intersections
33892+
// that include {} (we know that the other types in such intersections are assignable to object
33893+
// since we already checked for that).
33894+
if (hasEmptyObjectIntersection(rightType)) {
33895+
error(right, Diagnostics.Type_0_may_represent_a_primitive_value_which_is_not_permitted_as_the_right_operand_of_the_in_operator, typeToString(rightType));
3387333896
}
3387433897
}
33875-
rightType = checkNonNullType(rightType, right);
33876-
// TypeScript 1.0 spec (April 2014): 4.15.5
33877-
// The in operator requires the right operand to be
33878-
//
33879-
// 1. assignable to the non-primitive type,
33880-
// 2. an unconstrained type parameter,
33881-
// 3. a union or intersection including one or more type parameters, whose constituents are all assignable to the
33882-
// the non-primitive type, or are unconstrainted type parameters, or have constraints assignable to the
33883-
// non-primitive type, or
33884-
// 4. a type parameter whose constraint is
33885-
// i. an object type,
33886-
// ii. the non-primitive type, or
33887-
// iii. a union or intersection with at least one constituent assignable to an object or non-primitive type.
33888-
//
33889-
// The divergent behavior for type parameters and unions containing type parameters is a workaround for type
33890-
// parameters not being narrowable. If the right operand is a concrete type, we can error if there is any chance
33891-
// it is a primitive. But if the operand is a type parameter, it cannot be narrowed, so we don't issue an error
33892-
// unless *all* instantiations would result in an error.
33893-
//
3389433898
// The result is always of the Boolean primitive type.
33895-
const rightTypeConstraint = getConstraintOfType(rightType);
33896-
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
33897-
rightTypeConstraint && (
33898-
isTypeAssignableToKind(rightType, TypeFlags.UnionOrIntersection) && !allTypesAssignableToKind(rightTypeConstraint, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
33899-
!maybeTypeOfKind(rightTypeConstraint, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object)
33900-
)
33901-
) {
33902-
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive);
33903-
}
3390433899
return booleanType;
3390533900
}
3390633901

‎src/compiler/diagnosticMessages.json

+4-8
Original file line numberDiff line numberDiff line change
@@ -1844,14 +1844,6 @@
18441844
"category": "Error",
18451845
"code": 2359
18461846
},
1847-
"The left-hand side of an 'in' expression must be a private identifier or of type 'any', 'string', 'number', or 'symbol'.": {
1848-
"category": "Error",
1849-
"code": 2360
1850-
},
1851-
"The right-hand side of an 'in' expression must not be a primitive.": {
1852-
"category": "Error",
1853-
"code": 2361
1854-
},
18551847
"The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.": {
18561848
"category": "Error",
18571849
"code": 2362
@@ -2845,6 +2837,10 @@
28452837
"category": "Error",
28462838
"code": 2637
28472839
},
2840+
"Type '{0}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.": {
2841+
"category": "Error",
2842+
"code": 2638
2843+
},
28482844

28492845
"Cannot augment module '{0}' with value exports because it resolves to a non-module entity.": {
28502846
"category": "Error",

0 commit comments

Comments
 (0)