'in' should not operate on primitive types (#41928 + @andrewbranch) (#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>
This commit is contained in:
Andrew Branch
2021-01-11 16:46:08 -08:00
committed by GitHub
co-authored by Jonas Hübotter
parent c456bbd466
commit b0f5e35777
10 changed files with 656 additions and 26 deletions
+25 -3
View File
@@ -30305,14 +30305,36 @@ namespace ts {
rightType = checkNonNullType(rightType, right);
// TypeScript 1.0 spec (April 2014): 4.15.5
// The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type,
// and the right operand to be of type Any, an object type, or a type parameter type.
// and the right operand to be
//
// 1. assignable to the non-primitive type,
// 2. an unconstrained type parameter,
// 3. a union or intersection including one or more type parameters, whose constituents are all assignable to the
// the non-primitive type, or are unconstrainted type parameters, or have constraints assignable to the
// non-primitive type, or
// 4. a type parameter whose constraint is
// i. an object type,
// ii. the non-primitive type, or
// iii. a union or intersection with at least one constituent assignable to an object or non-primitive type.
//
// The divergent behavior for type parameters and unions containing type parameters is a workaround for type
// parameters not being narrowable. If the right operand is a concrete type, we can error if there is any chance
// it is a primitive. But if the operand is a type parameter, it cannot be narrowed, so we don't issue an error
// unless *all* instantiations would result in an error.
//
// The result is always of the Boolean primitive type.
if (!(allTypesAssignableToKind(leftType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) ||
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) {
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol);
}
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive)) {
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
const rightTypeConstraint = getConstraintOfType(rightType);
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
rightTypeConstraint && (
isTypeAssignableToKind(rightType, TypeFlags.UnionOrIntersection) && !allTypesAssignableToKind(rightTypeConstraint, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
!maybeTypeOfKind(rightTypeConstraint, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object)
)
) {
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive);
}
return booleanType;
}
+1 -1
View File
@@ -1625,7 +1625,7 @@
"category": "Error",
"code": 2360
},
"The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.": {
"The right-hand side of an 'in' expression must not be a primitive.": {
"category": "Error",
"code": 2361
},
@@ -0,0 +1,93 @@
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(19,17): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(23,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(27,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(34,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(53,14): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(55,18): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(60,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts(64,12): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
==== tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts (8 errors) ====
const validHasKey = <T extends object>(
thing: T,
key: string,
): boolean => {
return key in thing; // Ok
};
const alsoValidHasKey = <T>(
thing: T,
key: string,
): boolean => {
return key in thing; // Ok (as T may be instantiated with a valid type)
};
function invalidHasKey<T extends string | number>(
thing: T,
key: string,
): boolean {
return key in thing; // Error (because all possible instantiations are errors)
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
}
function union1<T extends string | number, U extends boolean>(thing: T | U) {
"key" in thing; // Error (because all possible instantiations are errors)
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
}
function union2<T extends object, U extends string | number>(thing: T | U) {
"key" in thing; // Error (because narrowing is possible)
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
if (typeof thing === "object") {
"key" in thing; // Ok
}
}
function union3<T>(thing: T | string | number) {
"key" in thing; // Error (because narrowing is possible)
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
if (typeof thing !== "string" && typeof thing !== "number") {
"key" in thing; // Ok (because further narrowing is impossible)
}
}
function union4<T extends object | "hello">(thing: T) {
"key" in thing; // Ok (because narrowing is impossible)
}
function union5<T extends object | string, U extends object | number>(p: T | U) {
// For consistency, this should probably not be an error, because useful
// narrowing is impossible. However, this is exceptionally strange input,
// and it adds a lot of complexity to distinguish between a `T | U` where
// one constraint is non-primitive and the other is primitive and a `T | U`
// like this where both constraints have primitive and non-primitive
// constitutents. Also, the strictly sound behavior would be to error
// here, which is what's happening, so "fixing" this by suppressing the
// error seems very low-value.
"key" in p;
~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
if (typeof p === "object") {
"key" in p;
~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
}
}
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
"key" in thing; // Error (because all possible instantiations are errors)
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
}
function intersection2<T>(thing: T & (0 | 1 | 2)) {
"key" in thing; // Error (because all possible instantations are errors)
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
}
@@ -0,0 +1,116 @@
//// [inDoesNotOperateOnPrimitiveTypes.ts]
const validHasKey = <T extends object>(
thing: T,
key: string,
): boolean => {
return key in thing; // Ok
};
const alsoValidHasKey = <T>(
thing: T,
key: string,
): boolean => {
return key in thing; // Ok (as T may be instantiated with a valid type)
};
function invalidHasKey<T extends string | number>(
thing: T,
key: string,
): boolean {
return key in thing; // Error (because all possible instantiations are errors)
}
function union1<T extends string | number, U extends boolean>(thing: T | U) {
"key" in thing; // Error (because all possible instantiations are errors)
}
function union2<T extends object, U extends string | number>(thing: T | U) {
"key" in thing; // Error (because narrowing is possible)
if (typeof thing === "object") {
"key" in thing; // Ok
}
}
function union3<T>(thing: T | string | number) {
"key" in thing; // Error (because narrowing is possible)
if (typeof thing !== "string" && typeof thing !== "number") {
"key" in thing; // Ok (because further narrowing is impossible)
}
}
function union4<T extends object | "hello">(thing: T) {
"key" in thing; // Ok (because narrowing is impossible)
}
function union5<T extends object | string, U extends object | number>(p: T | U) {
// For consistency, this should probably not be an error, because useful
// narrowing is impossible. However, this is exceptionally strange input,
// and it adds a lot of complexity to distinguish between a `T | U` where
// one constraint is non-primitive and the other is primitive and a `T | U`
// like this where both constraints have primitive and non-primitive
// constitutents. Also, the strictly sound behavior would be to error
// here, which is what's happening, so "fixing" this by suppressing the
// error seems very low-value.
"key" in p;
if (typeof p === "object") {
"key" in p;
}
}
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
"key" in thing; // Error (because all possible instantiations are errors)
}
function intersection2<T>(thing: T & (0 | 1 | 2)) {
"key" in thing; // Error (because all possible instantations are errors)
}
//// [inDoesNotOperateOnPrimitiveTypes.js]
var validHasKey = function (thing, key) {
return key in thing; // Ok
};
var alsoValidHasKey = function (thing, key) {
return key in thing; // Ok (as T may be instantiated with a valid type)
};
function invalidHasKey(thing, key) {
return key in thing; // Error (because all possible instantiations are errors)
}
function union1(thing) {
"key" in thing; // Error (because all possible instantiations are errors)
}
function union2(thing) {
"key" in thing; // Error (because narrowing is possible)
if (typeof thing === "object") {
"key" in thing; // Ok
}
}
function union3(thing) {
"key" in thing; // Error (because narrowing is possible)
if (typeof thing !== "string" && typeof thing !== "number") {
"key" in thing; // Ok (because further narrowing is impossible)
}
}
function union4(thing) {
"key" in thing; // Ok (because narrowing is impossible)
}
function union5(p) {
// For consistency, this should probably not be an error, because useful
// narrowing is impossible. However, this is exceptionally strange input,
// and it adds a lot of complexity to distinguish between a `T | U` where
// one constraint is non-primitive and the other is primitive and a `T | U`
// like this where both constraints have primitive and non-primitive
// constitutents. Also, the strictly sound behavior would be to error
// here, which is what's happening, so "fixing" this by suppressing the
// error seems very low-value.
"key" in p;
if (typeof p === "object") {
"key" in p;
}
}
function intersection1(thing) {
"key" in thing; // Error (because all possible instantiations are errors)
}
function intersection2(thing) {
"key" in thing; // Error (because all possible instantations are errors)
}
@@ -0,0 +1,162 @@
=== tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts ===
const validHasKey = <T extends object>(
>validHasKey : Symbol(validHasKey, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 0, 5))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 0, 21))
thing: T,
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 0, 39))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 0, 21))
key: string,
>key : Symbol(key, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 1, 11))
): boolean => {
return key in thing; // Ok
>key : Symbol(key, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 1, 11))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 0, 39))
};
const alsoValidHasKey = <T>(
>alsoValidHasKey : Symbol(alsoValidHasKey, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 7, 5))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 7, 25))
thing: T,
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 7, 28))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 7, 25))
key: string,
>key : Symbol(key, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 8, 11))
): boolean => {
return key in thing; // Ok (as T may be instantiated with a valid type)
>key : Symbol(key, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 8, 11))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 7, 28))
};
function invalidHasKey<T extends string | number>(
>invalidHasKey : Symbol(invalidHasKey, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 12, 2))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 14, 23))
thing: T,
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 14, 50))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 14, 23))
key: string,
>key : Symbol(key, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 15, 11))
): boolean {
return key in thing; // Error (because all possible instantiations are errors)
>key : Symbol(key, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 15, 11))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 14, 50))
}
function union1<T extends string | number, U extends boolean>(thing: T | U) {
>union1 : Symbol(union1, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 19, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 21, 16))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 21, 42))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 21, 62))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 21, 16))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 21, 42))
"key" in thing; // Error (because all possible instantiations are errors)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 21, 62))
}
function union2<T extends object, U extends string | number>(thing: T | U) {
>union2 : Symbol(union2, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 23, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 16))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 33))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 61))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 16))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 33))
"key" in thing; // Error (because narrowing is possible)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 61))
if (typeof thing === "object") {
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 61))
"key" in thing; // Ok
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 25, 61))
}
}
function union3<T>(thing: T | string | number) {
>union3 : Symbol(union3, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 30, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 16))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 19))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 16))
"key" in thing; // Error (because narrowing is possible)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 19))
if (typeof thing !== "string" && typeof thing !== "number") {
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 19))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 19))
"key" in thing; // Ok (because further narrowing is impossible)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 32, 19))
}
}
function union4<T extends object | "hello">(thing: T) {
>union4 : Symbol(union4, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 37, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 39, 16))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 39, 44))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 39, 16))
"key" in thing; // Ok (because narrowing is impossible)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 39, 44))
}
function union5<T extends object | string, U extends object | number>(p: T | U) {
>union5 : Symbol(union5, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 41, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 16))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 42))
>p : Symbol(p, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 70))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 16))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 42))
// For consistency, this should probably not be an error, because useful
// narrowing is impossible. However, this is exceptionally strange input,
// and it adds a lot of complexity to distinguish between a `T | U` where
// one constraint is non-primitive and the other is primitive and a `T | U`
// like this where both constraints have primitive and non-primitive
// constitutents. Also, the strictly sound behavior would be to error
// here, which is what's happening, so "fixing" this by suppressing the
// error seems very low-value.
"key" in p;
>p : Symbol(p, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 70))
if (typeof p === "object") {
>p : Symbol(p, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 70))
"key" in p;
>p : Symbol(p, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 43, 70))
}
}
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
>intersection1 : Symbol(intersection1, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 56, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 58, 23))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 58, 40))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 58, 62))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 58, 23))
>U : Symbol(U, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 58, 40))
"key" in thing; // Error (because all possible instantiations are errors)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 58, 62))
}
function intersection2<T>(thing: T & (0 | 1 | 2)) {
>intersection2 : Symbol(intersection2, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 60, 1))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 62, 23))
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 62, 26))
>T : Symbol(T, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 62, 23))
"key" in thing; // Error (because all possible instantations are errors)
>thing : Symbol(thing, Decl(inDoesNotOperateOnPrimitiveTypes.ts, 62, 26))
}
@@ -0,0 +1,172 @@
=== tests/cases/compiler/inDoesNotOperateOnPrimitiveTypes.ts ===
const validHasKey = <T extends object>(
>validHasKey : <T extends object>(thing: T, key: string) => boolean
><T extends object>( thing: T, key: string,): boolean => { return key in thing; // Ok} : <T extends object>(thing: T, key: string) => boolean
thing: T,
>thing : T
key: string,
>key : string
): boolean => {
return key in thing; // Ok
>key in thing : boolean
>key : string
>thing : T
};
const alsoValidHasKey = <T>(
>alsoValidHasKey : <T>(thing: T, key: string) => boolean
><T>( thing: T, key: string,): boolean => { return key in thing; // Ok (as T may be instantiated with a valid type)} : <T>(thing: T, key: string) => boolean
thing: T,
>thing : T
key: string,
>key : string
): boolean => {
return key in thing; // Ok (as T may be instantiated with a valid type)
>key in thing : boolean
>key : string
>thing : T
};
function invalidHasKey<T extends string | number>(
>invalidHasKey : <T extends string | number>(thing: T, key: string) => boolean
thing: T,
>thing : T
key: string,
>key : string
): boolean {
return key in thing; // Error (because all possible instantiations are errors)
>key in thing : boolean
>key : string
>thing : T
}
function union1<T extends string | number, U extends boolean>(thing: T | U) {
>union1 : <T extends string | number, U extends boolean>(thing: T | U) => void
>thing : T | U
"key" in thing; // Error (because all possible instantiations are errors)
>"key" in thing : boolean
>"key" : "key"
>thing : T | U
}
function union2<T extends object, U extends string | number>(thing: T | U) {
>union2 : <T extends object, U extends string | number>(thing: T | U) => void
>thing : T | U
"key" in thing; // Error (because narrowing is possible)
>"key" in thing : boolean
>"key" : "key"
>thing : T | U
if (typeof thing === "object") {
>typeof thing === "object" : boolean
>typeof thing : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>thing : T | U
>"object" : "object"
"key" in thing; // Ok
>"key" in thing : boolean
>"key" : "key"
>thing : T
}
}
function union3<T>(thing: T | string | number) {
>union3 : <T>(thing: T | string | number) => void
>thing : string | number | T
"key" in thing; // Error (because narrowing is possible)
>"key" in thing : boolean
>"key" : "key"
>thing : string | number | T
if (typeof thing !== "string" && typeof thing !== "number") {
>typeof thing !== "string" && typeof thing !== "number" : boolean
>typeof thing !== "string" : boolean
>typeof thing : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>thing : string | number | T
>"string" : "string"
>typeof thing !== "number" : boolean
>typeof thing : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>thing : number | T
>"number" : "number"
"key" in thing; // Ok (because further narrowing is impossible)
>"key" in thing : boolean
>"key" : "key"
>thing : T
}
}
function union4<T extends object | "hello">(thing: T) {
>union4 : <T extends object | "hello">(thing: T) => void
>thing : T
"key" in thing; // Ok (because narrowing is impossible)
>"key" in thing : boolean
>"key" : "key"
>thing : T
}
function union5<T extends object | string, U extends object | number>(p: T | U) {
>union5 : <T extends string | object, U extends number | object>(p: T | U) => void
>p : T | U
// For consistency, this should probably not be an error, because useful
// narrowing is impossible. However, this is exceptionally strange input,
// and it adds a lot of complexity to distinguish between a `T | U` where
// one constraint is non-primitive and the other is primitive and a `T | U`
// like this where both constraints have primitive and non-primitive
// constitutents. Also, the strictly sound behavior would be to error
// here, which is what's happening, so "fixing" this by suppressing the
// error seems very low-value.
"key" in p;
>"key" in p : boolean
>"key" : "key"
>p : T | U
if (typeof p === "object") {
>typeof p === "object" : boolean
>typeof p : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>p : T | U
>"object" : "object"
"key" in p;
>"key" in p : boolean
>"key" : "key"
>p : T | U
}
}
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
>intersection1 : <T extends number, U extends 0 | 1 | 2>(thing: T & U) => void
>thing : T & U
"key" in thing; // Error (because all possible instantiations are errors)
>"key" in thing : boolean
>"key" : "key"
>thing : T & U
}
function intersection2<T>(thing: T & (0 | 1 | 2)) {
>intersection2 : <T>(thing: T & (0 | 1 | 2)) => void
>thing : T & (0 | 1 | 2)
"key" in thing; // Error (because all possible instantations are errors)
>"key" in thing : boolean
>"key" : "key"
>thing : T & (0 | 1 | 2)
}
@@ -1,4 +1,4 @@
tests/cases/compiler/inOperator.ts(7,15): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/compiler/inOperator.ts(7,15): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
==== tests/cases/compiler/inOperator.ts (1 errors) ====
@@ -10,7 +10,7 @@ tests/cases/compiler/inOperator.ts(7,15): error TS2361: The right-hand side of a
var b = '' in 0;
~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var c: any;
var y: number;
@@ -7,18 +7,18 @@ tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInv
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(23,11): error TS2360: The left-hand side of an 'in' expression must be of type 'any', 'string', 'number', or 'symbol'.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(24,12): error TS2360: The left-hand side of an 'in' expression must be of type 'any', 'string', 'number', or 'symbol'.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(25,12): error TS2360: The left-hand side of an 'in' expression must be of type 'any', 'string', 'number', or 'symbol'.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(35,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(36,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(37,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(38,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(39,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(40,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(41,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(42,16): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(35,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(36,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(37,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(38,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(39,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(40,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(41,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(42,16): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(43,16): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(44,17): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(47,11): error TS2360: The left-hand side of an 'in' expression must be of type 'any', 'string', 'number', or 'symbol'.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(47,17): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts(47,17): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
==== tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInvalidOperands.ts (21 errors) ====
@@ -76,28 +76,28 @@ tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInv
var rb1 = x in b1;
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb2 = x in b2;
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb3 = x in b3;
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb4 = x in b4;
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb5 = x in b5;
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb6 = x in 0;
~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb7 = x in false;
~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb8 = x in '';
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
var rb9 = x in null;
~~~~
!!! error TS2531: Object is possibly 'null'.
@@ -110,4 +110,4 @@ tests/cases/conformance/expressions/binaryOperators/inOperator/inOperatorWithInv
~~
!!! error TS2360: The left-hand side of an 'in' expression must be of type 'any', 'string', 'number', or 'symbol'.
~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
@@ -1,8 +1,8 @@
tests/cases/conformance/es6/Symbols/symbolType2.ts(2,7): error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
tests/cases/conformance/es6/Symbols/symbolType2.ts(2,7): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
==== tests/cases/conformance/es6/Symbols/symbolType2.ts (1 errors) ====
Symbol.isConcatSpreadable in {};
"" in Symbol.toPrimitive;
~~~~~~~~~~~~~~~~~~
!!! error TS2361: The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter.
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
@@ -0,0 +1,65 @@
const validHasKey = <T extends object>(
thing: T,
key: string,
): boolean => {
return key in thing; // Ok
};
const alsoValidHasKey = <T>(
thing: T,
key: string,
): boolean => {
return key in thing; // Ok (as T may be instantiated with a valid type)
};
function invalidHasKey<T extends string | number>(
thing: T,
key: string,
): boolean {
return key in thing; // Error (because all possible instantiations are errors)
}
function union1<T extends string | number, U extends boolean>(thing: T | U) {
"key" in thing; // Error (because all possible instantiations are errors)
}
function union2<T extends object, U extends string | number>(thing: T | U) {
"key" in thing; // Error (because narrowing is possible)
if (typeof thing === "object") {
"key" in thing; // Ok
}
}
function union3<T>(thing: T | string | number) {
"key" in thing; // Error (because narrowing is possible)
if (typeof thing !== "string" && typeof thing !== "number") {
"key" in thing; // Ok (because further narrowing is impossible)
}
}
function union4<T extends object | "hello">(thing: T) {
"key" in thing; // Ok (because narrowing is impossible)
}
function union5<T extends object | string, U extends object | number>(p: T | U) {
// For consistency, this should probably not be an error, because useful
// narrowing is impossible. However, this is exceptionally strange input,
// and it adds a lot of complexity to distinguish between a `T | U` where
// one constraint is non-primitive and the other is primitive and a `T | U`
// like this where both constraints have primitive and non-primitive
// constitutents. Also, the strictly sound behavior would be to error
// here, which is what's happening, so "fixing" this by suppressing the
// error seems very low-value.
"key" in p;
if (typeof p === "object") {
"key" in p;
}
}
function intersection1<T extends number, U extends 0 | 1 | 2>(thing: T & U) {
"key" in thing; // Error (because all possible instantiations are errors)
}
function intersection2<T>(thing: T & (0 | 1 | 2)) {
"key" in thing; // Error (because all possible instantations are errors)
}