Skip to content

Commit b0f5e35

Browse files
andrewbranchjonhue
andauthored
'in' should not operate on primitive types (microsoft#41928 + @andrewbranch) (microsoft#42288)
* 'in' should not operate on primitive types * accept baselines of failing tests * review * update error message * check if constraint of right type is assignable to a non primitive or instantiable non primitive * do not throw errors where narrowing is impossible * accept baselines * fix test case failures * Add more accurate comment discussion and document failing edge case in test * Update baselines Co-authored-by: Jonas Hübotter <jonas.huebotter@gmail.com>
1 parent c456bbd commit b0f5e35

10 files changed

+656
-26
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30305,14 +30305,36 @@ namespace ts {
3030530305
rightType = checkNonNullType(rightType, right);
3030630306
// TypeScript 1.0 spec (April 2014): 4.15.5
3030730307
// The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type,
30308-
// and the right operand to be of type Any, an object type, or a type parameter type.
30308+
// and the right operand to be
30309+
//
30310+
// 1. assignable to the non-primitive type,
30311+
// 2. an unconstrained type parameter,
30312+
// 3. a union or intersection including one or more type parameters, whose constituents are all assignable to the
30313+
// the non-primitive type, or are unconstrainted type parameters, or have constraints assignable to the
30314+
// non-primitive type, or
30315+
// 4. a type parameter whose constraint is
30316+
// i. an object type,
30317+
// ii. the non-primitive type, or
30318+
// iii. a union or intersection with at least one constituent assignable to an object or non-primitive type.
30319+
//
30320+
// The divergent behavior for type parameters and unions containing type parameters is a workaround for type
30321+
// parameters not being narrowable. If the right operand is a concrete type, we can error if there is any chance
30322+
// it is a primitive. But if the operand is a type parameter, it cannot be narrowed, so we don't issue an error
30323+
// unless *all* instantiations would result in an error.
30324+
//
3030930325
// The result is always of the Boolean primitive type.
3031030326
if (!(allTypesAssignableToKind(leftType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) ||
3031130327
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) {
3031230328
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol);
3031330329
}
30314-
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive)) {
30315-
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
30330+
const rightTypeConstraint = getConstraintOfType(rightType);
30331+
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
30332+
rightTypeConstraint && (
30333+
isTypeAssignableToKind(rightType, TypeFlags.UnionOrIntersection) && !allTypesAssignableToKind(rightTypeConstraint, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
30334+
!maybeTypeOfKind(rightTypeConstraint, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object)
30335+
)
30336+
) {
30337+
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive);
3031630338
}
3031730339
return booleanType;
3031830340
}

src/compiler/diagnosticMessages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,7 @@
16251625
"category": "Error",
16261626
"code": 2360
16271627
},
1628-
"The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.": {
1628+
"The right-hand side of an 'in' expression must not be a primitive.": {
16291629
"category": "Error",
16301630
"code": 2361
16311631
},
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(19,17): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
2+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(23,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
3+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(27,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
4+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(34,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
5+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(53,14): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
6+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(55,18): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
7+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(60,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
8+
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(64,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
9+
10+
11+
==== tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts (8 errors) ====
12+
const validHasKey = <T extends object>(
13+
thing: T,
14+
key: string,
15+
): boolean => {
16+
return key in thing; // Ok
17+
};
18+
19+
const alsoValidHasKey = <T>(
20+
thing: T,
21+
key: string,
22+
): boolean => {
23+
return key in thing; // Ok (as T may be instantiated with a valid type)
24+
};
25+
26+
function invalidHasKey<T extends string | number>(
27+
thing: T,
28+
key: string,
29+
): boolean {
30+
return key in thing; // Error (because all possible instantiations are errors)
31+
~~~~~
32+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
33+
}
34+
35+
function union1<T extends string | number, U extends boolean>(thing: T | U) {
36+
"key" in thing; // Error (because all possible instantiations are errors)
37+
~~~~~
38+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
39+
}
40+
41+
function union2<T extends object, U extends string | number>(thing: T | U) {
42+
"key" in thing; // Error (because narrowing is possible)
43+
~~~~~
44+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
45+
if (typeof thing === "object") {
46+
"key" in thing; // Ok
47+
}
48+
}
49+
50+
function union3<T>(thing: T | string | number) {
51+
"key" in thing; // Error (because narrowing is possible)
52+
~~~~~
53+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
54+
if (typeof thing !== "string" && typeof thing !== "number") {
55+
"key" in thing; // Ok (because further narrowing is impossible)
56+
}
57+
}
58+
59+
function union4<T extends object | "hello">(thing: T) {
60+
"key" in thing; // Ok (because narrowing is impossible)
61+
}
62+
63+
function union5<T extends object | string, U extends object | number>(p: T | U) {
64+
// For consistency, this should probably not be an error, because useful
65+
// narrowing is impossible. However, this is exceptionally strange input,
66+
// and it adds a lot of complexity to distinguish between a `T | U` where
67+
// one constraint is non-primitive and the other is primitive and a `T | U`
68+
// like this where both constraints have primitive and non-primitive
69+
// constitutents. Also, the strictly sound behavior would be to error
70+
// here, which is what's happening, so "fixing" this by suppressing the
71+
// error seems very low-value.
72+
"key" in p;
73+
~
74+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
75+
if (typeof p === "object") {
76+
"key" in p;
77+
~
78+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
79+
}
80+
}
81+
82+
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
83+
"key" in thing; // Error (because all possible instantiations are errors)
84+
~~~~~
85+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
86+
}
87+
88+
function intersection2<T>(thing: T & (0 | 1 | 2)) {
89+
"key" in thing; // Error (because all possible instantations are errors)
90+
~~~~~
91+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
92+
}
93+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//// [inDoesNotOperateOnPrimitiveTypes.ts]
2+
const validHasKey = <T extends object>(
3+
thing: T,
4+
key: string,
5+
): boolean => {
6+
return key in thing; // Ok
7+
};
8+
9+
const alsoValidHasKey = <T>(
10+
thing: T,
11+
key: string,
12+
): boolean => {
13+
return key in thing; // Ok (as T may be instantiated with a valid type)
14+
};
15+
16+
function invalidHasKey<T extends string | number>(
17+
thing: T,
18+
key: string,
19+
): boolean {
20+
return key in thing; // Error (because all possible instantiations are errors)
21+
}
22+
23+
function union1<T extends string | number, U extends boolean>(thing: T | U) {
24+
"key" in thing; // Error (because all possible instantiations are errors)
25+
}
26+
27+
function union2<T extends object, U extends string | number>(thing: T | U) {
28+
"key" in thing; // Error (because narrowing is possible)
29+
if (typeof thing === "object") {
30+
"key" in thing; // Ok
31+
}
32+
}
33+
34+
function union3<T>(thing: T | string | number) {
35+
"key" in thing; // Error (because narrowing is possible)
36+
if (typeof thing !== "string" && typeof thing !== "number") {
37+
"key" in thing; // Ok (because further narrowing is impossible)
38+
}
39+
}
40+
41+
function union4<T extends object | "hello">(thing: T) {
42+
"key" in thing; // Ok (because narrowing is impossible)
43+
}
44+
45+
function union5<T extends object | string, U extends object | number>(p: T | U) {
46+
// For consistency, this should probably not be an error, because useful
47+
// narrowing is impossible. However, this is exceptionally strange input,
48+
// and it adds a lot of complexity to distinguish between a `T | U` where
49+
// one constraint is non-primitive and the other is primitive and a `T | U`
50+
// like this where both constraints have primitive and non-primitive
51+
// constitutents. Also, the strictly sound behavior would be to error
52+
// here, which is what's happening, so "fixing" this by suppressing the
53+
// error seems very low-value.
54+
"key" in p;
55+
if (typeof p === "object") {
56+
"key" in p;
57+
}
58+
}
59+
60+
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
61+
"key" in thing; // Error (because all possible instantiations are errors)
62+
}
63+
64+
function intersection2<T>(thing: T & (0 | 1 | 2)) {
65+
"key" in thing; // Error (because all possible instantations are errors)
66+
}
67+
68+
69+
//// [inDoesNotOperateOnPrimitiveTypes.js]
70+
var validHasKey = function (thing, key) {
71+
return key in thing; // Ok
72+
};
73+
var alsoValidHasKey = function (thing, key) {
74+
return key in thing; // Ok (as T may be instantiated with a valid type)
75+
};
76+
function invalidHasKey(thing, key) {
77+
return key in thing; // Error (because all possible instantiations are errors)
78+
}
79+
function union1(thing) {
80+
"key" in thing; // Error (because all possible instantiations are errors)
81+
}
82+
function union2(thing) {
83+
"key" in thing; // Error (because narrowing is possible)
84+
if (typeof thing === "object") {
85+
"key" in thing; // Ok
86+
}
87+
}
88+
function union3(thing) {
89+
"key" in thing; // Error (because narrowing is possible)
90+
if (typeof thing !== "string" && typeof thing !== "number") {
91+
"key" in thing; // Ok (because further narrowing is impossible)
92+
}
93+
}
94+
function union4(thing) {
95+
"key" in thing; // Ok (because narrowing is impossible)
96+
}
97+
function union5(p) {
98+
// For consistency, this should probably not be an error, because useful
99+
// narrowing is impossible. However, this is exceptionally strange input,
100+
// and it adds a lot of complexity to distinguish between a `T | U` where
101+
// one constraint is non-primitive and the other is primitive and a `T | U`
102+
// like this where both constraints have primitive and non-primitive
103+
// constitutents. Also, the strictly sound behavior would be to error
104+
// here, which is what's happening, so "fixing" this by suppressing the
105+
// error seems very low-value.
106+
"key" in p;
107+
if (typeof p === "object") {
108+
"key" in p;
109+
}
110+
}
111+
function intersection1(thing) {
112+
"key" in thing; // Error (because all possible instantiations are errors)
113+
}
114+
function intersection2(thing) {
115+
"key" in thing; // Error (because all possible instantations are errors)
116+
}

0 commit comments

Comments
 (0)