Skip to content

Commit 161d1f1

Browse files
committed
Add tests for instanceof and narrowing
1 parent 69f59da commit 161d1f1

14 files changed

+1811
-25
lines changed

src/compiler/checker.ts

+42-22
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ import {
381381
HasIllegalModifiers,
382382
HasInitializer,
383383
hasInitializer,
384+
HasInstanceMethodType,
384385
hasJSDocNodes,
385386
hasJSDocParameterTags,
386387
hasJsonModuleEmitEnabled,
@@ -2186,6 +2187,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
21862187
var potentialReflectCollisions: Node[] = [];
21872188
var potentialUnusedRenamedBindingElementsInTypes: BindingElement[] = [];
21882189
var awaitedTypeStack: number[] = [];
2190+
var hasGlobalSymbolHasInstanceProperty: boolean | undefined;
21892191

21902192
var diagnostics = createDiagnosticCollection();
21912193
var suggestionDiagnostics = createDiagnosticCollection();
@@ -27467,9 +27469,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2746727469
// if rightType is an object type with a custom `[Symbol.hasInstance]` method, and that method has a type
2746827470
// predicate, use the type predicate to perform narrowing. This allows normal `object` types to participate
2746927471
// in `instanceof`, as per Step 2 of https://tc39.es/ecma262/#sec-instanceofoperator.
27470-
const customHasInstanceMethodType = getCustomSymbolHasInstanceMethodOfObjectType(rightType);
27471-
if (customHasInstanceMethodType) {
27472-
const syntheticCall = createSyntheticHasInstanceMethodCall(left, expr.right, type, customHasInstanceMethodType);
27472+
const hasInstanceMethodType = getSymbolHasInstanceMethodOfObjectType(rightType);
27473+
if (hasInstanceMethodType) {
27474+
const syntheticCall = createSyntheticHasInstanceMethodCall(left, expr.right, type, hasInstanceMethodType);
2747327475
const signature = getEffectsSignature(syntheticCall);
2747427476
const predicate = signature && getTypePredicateOfSignature(signature);
2747527477
if (predicate && (predicate.kind === TypePredicateKind.This || predicate.kind === TypePredicateKind.Identifier)) {
@@ -36475,13 +36477,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3647536477
}
3647636478

3647736479
/**
36478-
* Get the type of the `[Symbol.hasInstance]` method of an object type, but only if it is not the
36479-
* `[Symbol.hasInstance]` method inherited from the global `Function` type.
36480+
* Get the type of the `[Symbol.hasInstance]` method of an object type.
3648036481
*/
36481-
function getCustomSymbolHasInstanceMethodOfObjectType(type: Type) {
36482+
function getSymbolHasInstanceMethodOfObjectType(type: Type) {
3648236483
const hasInstancePropertyName = getPropertyNameForKnownSymbolName("hasInstance");
3648336484
const hasInstanceProperty = getPropertyOfObjectType(type, hasInstancePropertyName);
36484-
if (hasInstanceProperty && hasInstanceProperty !== getPropertyOfObjectType(globalFunctionType, hasInstancePropertyName)) {
36485+
if (hasInstanceProperty) {
3648536486
const hasInstancePropertyType = getTypeOfSymbol(hasInstanceProperty);
3648636487
if (hasInstancePropertyType && getSignaturesOfType(hasInstancePropertyType, SignatureKind.Call).length !== 0) {
3648736488
return hasInstancePropertyType;
@@ -36537,24 +36538,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3653736538
// if rightType is an object type with a custom `[Symbol.hasInstance]` method, then it is potentially
3653836539
// valid on the right-hand side of the `instanceof` operator. This allows normal `object` types to
3653936540
// participate in `instanceof`, as per Step 2 of https://tc39.es/ecma262/#sec-instanceofoperator.
36540-
const customHasInstanceMethodType = getCustomSymbolHasInstanceMethodOfObjectType(rightType);
36541-
if (customHasInstanceMethodType) {
36542-
// If rightType has a `[Symbol.hasInstance]` method that is not the default [Symbol.hasInstance]() method on `Function`, check
36543-
// that left is assignable to the first parameter.
36544-
const syntheticCall = createSyntheticHasInstanceMethodCall(left, right, leftType, customHasInstanceMethodType);
36545-
const returnType = getReturnTypeOfSignature(getResolvedSignature(syntheticCall));
36546-
36547-
// Also verify that the return type of the `[Symbol.hasInstance]` method is assignable to `boolean`. The spec
36548-
// will perform `ToBoolean` on the result, but this is more type-safe.
36549-
checkTypeAssignableTo(returnType, booleanType, right, Diagnostics.An_object_s_Symbol_hasInstance_method_must_return_a_boolean_value_for_it_to_be_used_on_the_right_hand_side_of_an_instanceof_expression);
36541+
const hasInstanceMethodType = getSymbolHasInstanceMethodOfObjectType(rightType);
36542+
if (hasInstanceMethodType) {
36543+
// avoid a complex check for every `instanceof` when the `[Symbol.hasInstance]` method has a single
36544+
// call signature that neither restricts nor narrows (via type predicate) the LHS value, e.g.
36545+
// `(value: unknown) => boolean`.
36546+
const cache = hasInstanceMethodType as HasInstanceMethodType;
36547+
if (cache.hasSimpleUnrestrictedSingleCallSignature === undefined) {
36548+
const signature = getSingleCallSignature(hasInstanceMethodType);
36549+
cache.hasSimpleUnrestrictedSingleCallSignature = !!signature && signature.parameters.length === 1 &&
36550+
!!(getTypeOfSymbol(signature.parameters[0]).flags & TypeFlags.AnyOrUnknown) &&
36551+
!!signature.resolvedReturnType &&
36552+
!!(signature.resolvedReturnType.flags & TypeFlags.Boolean);
36553+
}
36554+
if (!cache.hasSimpleUnrestrictedSingleCallSignature) {
36555+
// If rightType has a `[Symbol.hasInstance]` method that is not `(value: unknown) => boolean`, we
36556+
// must check the expression as if it were a call to `right[Symbol.hasInstance](left)1. The call to
36557+
// `getResolvedSignature`, below, will check that leftType is assignable to the type of the first
36558+
// parameter.
36559+
const syntheticCall = createSyntheticHasInstanceMethodCall(left, right, leftType, hasInstanceMethodType);
36560+
const returnType = getReturnTypeOfSignature(getResolvedSignature(syntheticCall));
36561+
36562+
// We also verify that the return type of the `[Symbol.hasInstance]` method is assignable to
36563+
// `boolean`. According to the spec, the runtime will actually perform `ToBoolean` on the result,
36564+
// but this is more type-safe.
36565+
checkTypeAssignableTo(returnType, booleanType, right, Diagnostics.An_object_s_Symbol_hasInstance_method_must_return_a_boolean_value_for_it_to_be_used_on_the_right_hand_side_of_an_instanceof_expression);
36566+
}
3655036567
}
3655136568
// NOTE: do not raise error if right is unknown as related error was already reported
36552-
if (!(customHasInstanceMethodType || typeHasCallOrConstructSignatures(rightType) || isTypeSubtypeOf(rightType, globalFunctionType))) {
36569+
if (!(hasInstanceMethodType || typeHasCallOrConstructSignatures(rightType) || isTypeSubtypeOf(rightType, globalFunctionType))) {
3655336570
// Do not indicate that `[Symbol.hasInstance]` is a valid option if it's not known to be present on `SymbolConstructor`.
36554-
const globalESSymbolConstructorSymbol = getGlobalESSymbolConstructorTypeSymbol(/*reportErrors*/ false);
36555-
const hasInstanceProp = globalESSymbolConstructorSymbol && getMembersOfSymbol(globalESSymbolConstructorSymbol).get(getPropertyNameForKnownSymbolName("hasInstance"));
36556-
const message = hasInstanceProp ?
36557-
Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_either_of_type_any_of_an_object_type_with_a_Symbol_hasInstance_method_or_of_a_type_assignable_to_the_Function_interface_type :
36571+
if (hasGlobalSymbolHasInstanceProperty === undefined) {
36572+
const globalESSymbolConstructorSymbol = getGlobalESSymbolConstructorTypeSymbol(/*reportErrors*/ false);
36573+
hasGlobalSymbolHasInstanceProperty = !!globalESSymbolConstructorSymbol && getMembersOfSymbol(globalESSymbolConstructorSymbol).has("hasInstance" as __String);
36574+
}
36575+
36576+
const message = hasGlobalSymbolHasInstanceProperty ?
36577+
Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_either_of_type_any_an_object_type_with_a_Symbol_hasInstance_method_or_a_type_assignable_to_the_Function_interface_type :
3655836578
Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_of_type_any_or_of_a_type_assignable_to_the_Function_interface_type;
3655936579
error(right, message);
3656036580
}

src/compiler/diagnosticMessages.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3667,7 +3667,7 @@
36673667
"category": "Error",
36683668
"code": 2856
36693669
},
3670-
"The right-hand side of an 'instanceof' expression must be either of type 'any', of an object type with a '[Symbol.hasInstance]()' method, or of a type assignable to the 'Function' interface type.": {
3670+
"The right-hand side of an 'instanceof' expression must be either of type 'any', an object type with a '[Symbol.hasInstance]()' method, or a type assignable to the 'Function' interface type.": {
36713671
"category": "Error",
36723672
"code": 2857
36733673
},

src/compiler/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6564,6 +6564,11 @@ export interface PromiseOrAwaitableType extends ObjectType, UnionType {
65646564
awaitedTypeOfType?: Type;
65656565
}
65666566

6567+
/** @internal */
6568+
export interface HasInstanceMethodType extends Type {
6569+
hasSimpleUnrestrictedSingleCallSignature?: boolean;
6570+
}
6571+
65676572
/** @internal */
65686573
export interface SyntheticDefaultModuleType extends Type {
65696574
syntheticType?: Type;

tests/baselines/reference/api/tsserverlibrary.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5139,7 +5139,7 @@ declare namespace ts {
51395139
readonly asteriskToken?: AsteriskToken;
51405140
readonly expression?: Expression;
51415141
}
5142-
interface SyntheticExpression extends Expression {
5142+
interface SyntheticExpression extends LeftHandSideExpression {
51435143
readonly kind: SyntaxKind.SyntheticExpression;
51445144
readonly isSpread: boolean;
51455145
readonly type: Type;

tests/baselines/reference/api/typescript.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,7 @@ declare namespace ts {
10861086
readonly asteriskToken?: AsteriskToken;
10871087
readonly expression?: Expression;
10881088
}
1089-
interface SyntheticExpression extends Expression {
1089+
interface SyntheticExpression extends LeftHandSideExpression {
10901090
readonly kind: SyntaxKind.SyntheticExpression;
10911091
readonly isSpread: boolean;
10921092
readonly type: Type;

0 commit comments

Comments
 (0)