Skip to content

Commit 69f59da

Browse files
committed
[WIP] Support custom 'Symbol.hasInstance' methods when checking/narrowing 'instanceof'
1 parent 94f03cf commit 69f59da

File tree

4 files changed

+122
-12
lines changed

4 files changed

+122
-12
lines changed

src/compiler/checker.ts

+108-11
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ import {
704704
isStringOrNumericLiteralLike,
705705
isSuperCall,
706706
isSuperProperty,
707+
isSyntheticExpression,
707708
isTaggedTemplateExpression,
708709
isTemplateSpan,
709710
isThisContainerOrFunctionBlock,
@@ -27463,6 +27464,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2746327464
return type;
2746427465
}
2746527466
const rightType = getTypeOfExpression(expr.right);
27467+
// if rightType is an object type with a custom `[Symbol.hasInstance]` method, and that method has a type
27468+
// predicate, use the type predicate to perform narrowing. This allows normal `object` types to participate
27469+
// 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);
27473+
const signature = getEffectsSignature(syntheticCall);
27474+
const predicate = signature && getTypePredicateOfSignature(signature);
27475+
if (predicate && (predicate.kind === TypePredicateKind.This || predicate.kind === TypePredicateKind.Identifier)) {
27476+
return narrowTypeByTypePredicate(type, predicate, syntheticCall, assumeTrue);
27477+
}
27478+
}
2746627479
if (!isTypeDerivedFrom(rightType, globalFunctionType)) {
2746727480
return type;
2746827481
}
@@ -27560,8 +27573,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2756027573
function narrowTypeByTypePredicate(type: Type, predicate: TypePredicate, callExpression: CallExpression, assumeTrue: boolean): Type {
2756127574
// Don't narrow from 'any' if the predicate type is exactly 'Object' or 'Function'
2756227575
if (predicate.type && !(isTypeAny(type) && (predicate.type === globalObjectType || predicate.type === globalFunctionType))) {
27563-
const predicateArgument = getTypePredicateArgument(predicate, callExpression);
27576+
let predicateArgument = getTypePredicateArgument(predicate, callExpression);
2756427577
if (predicateArgument) {
27578+
// If the predicate argument is synthetic and is the first argument of a synthetic call to
27579+
// `[Symbol.hasInstance]`, replace the synthetic predicate argument with the actual argument from
27580+
// the original `instanceof` expression which is stored as the synthetic argument's `parent`.
27581+
if (isSyntheticExpression(predicateArgument) &&
27582+
predicate.parameterIndex === 0 &&
27583+
isSyntheticHasInstanceMethodCall(callExpression)) {
27584+
Debug.assertNode(predicateArgument.parent, isExpression);
27585+
predicateArgument = predicateArgument.parent;
27586+
}
2756527587
if (isMatchingReference(reference, predicateArgument)) {
2756627588
return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false);
2756727589
}
@@ -33153,7 +33175,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3315333175
return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount);
3315433176
}
3315533177

33156-
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, headMessage?: DiagnosticMessage): Signature {
33178+
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, headMessageCallback?: () => DiagnosticMessage | undefined): Signature {
3315733179
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
3315833180
const isDecorator = node.kind === SyntaxKind.Decorator;
3315933181
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
@@ -33266,6 +33288,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3326633288
chain = chainDiagnosticMessages(chain, Diagnostics.The_last_overload_gave_the_following_error);
3326733289
chain = chainDiagnosticMessages(chain, Diagnostics.No_overload_matches_this_call);
3326833290
}
33291+
const headMessage = headMessageCallback?.();
3326933292
if (headMessage) {
3327033293
chain = chainDiagnosticMessages(chain, headMessage);
3327133294
}
@@ -33311,6 +33334,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3331133334
let chain = chainDiagnosticMessages(
3331233335
map(diags, createDiagnosticMessageChainFromDiagnostic),
3331333336
Diagnostics.No_overload_matches_this_call);
33337+
const headMessage = headMessageCallback?.();
3331433338
if (headMessage) {
3331533339
chain = chainDiagnosticMessages(chain, headMessage);
3331633340
}
@@ -33330,18 +33354,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3333033354
}
3333133355
}
3333233356
else if (candidateForArgumentArityError) {
33333-
diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args, headMessage));
33357+
diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args, headMessageCallback?.()));
3333433358
}
3333533359
else if (candidateForTypeArgumentError) {
33336-
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessage);
33360+
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessageCallback?.());
3333733361
}
3333833362
else {
3333933363
const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments));
3334033364
if (signaturesWithCorrectTypeArgumentArity.length === 0) {
33341-
diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments!, headMessage));
33365+
diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments!, headMessageCallback?.()));
3334233366
}
3334333367
else {
33344-
diagnostics.add(getArgumentArityError(node, signaturesWithCorrectTypeArgumentArity, args, headMessage));
33368+
diagnostics.add(getArgumentArityError(node, signaturesWithCorrectTypeArgumentArity, args, headMessageCallback?.()));
3334533369
}
3334633370
}
3334733371
}
@@ -33689,7 +33713,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3368933713
return resolveErrorCall(node);
3369033714
}
3369133715

33692-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags);
33716+
// If the call expression is a synthetic call to a `[Symbol.hasInstance]` method then we will produce a head
33717+
// message when reporting diagnostics that explains how we got to `right[Symbol.hasInstance](left)` from
33718+
// `left instanceof right`, as it pertains to "Argument" related messages reported for the call.
33719+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags, () => isSyntheticHasInstanceMethodCall(node) ?
33720+
Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_assignable_to_the_first_argument_of_the_right_hand_side_s_Symbol_hasInstance_method :
33721+
undefined);
3369333722
}
3369433723

3369533724
function isGenericFunctionReturningFunction(signature: Signature) {
@@ -34072,7 +34101,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3407234101
return resolveErrorCall(node);
3407334102
}
3407434103

34075-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, headMessage);
34104+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, () => headMessage);
3407634105
}
3407734106

3407834107
function createSignatureForJSXIntrinsic(node: JsxOpeningLikeElement, result: Type): Signature {
@@ -36445,6 +36474,52 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3644536474
return (symbol.flags & SymbolFlags.ConstEnum) !== 0;
3644636475
}
3644736476

36477+
/**
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+
*/
36481+
function getCustomSymbolHasInstanceMethodOfObjectType(type: Type) {
36482+
const hasInstancePropertyName = getPropertyNameForKnownSymbolName("hasInstance");
36483+
const hasInstanceProperty = getPropertyOfObjectType(type, hasInstancePropertyName);
36484+
if (hasInstanceProperty && hasInstanceProperty !== getPropertyOfObjectType(globalFunctionType, hasInstancePropertyName)) {
36485+
const hasInstancePropertyType = getTypeOfSymbol(hasInstanceProperty);
36486+
if (hasInstancePropertyType && getSignaturesOfType(hasInstancePropertyType, SignatureKind.Call).length !== 0) {
36487+
return hasInstancePropertyType;
36488+
}
36489+
}
36490+
}
36491+
36492+
/**
36493+
* Creates a synthetic `CallExpression` that reinterprets `left instanceof right` as `right[Symbol.hasInstance](left)`
36494+
* per the `InstanceofOperator` algorithm in the ECMAScript specification.
36495+
* @param left The left-hand expression of `instanceof`
36496+
* @param right The right-hand expression of `instanceof`
36497+
* @param leftType The type of the left-hand expression of `instanceof`.
36498+
* @param hasInstanceMethodType The type of the `[Symbol.hasInstance]` method of the right-hand expression of `instanceof`.
36499+
*/
36500+
function createSyntheticHasInstanceMethodCall(left: Expression, right: Expression, leftType: Type, hasInstanceMethodType: Type) {
36501+
const syntheticExpression = createSyntheticExpression(right, hasInstanceMethodType);
36502+
const syntheticArgument = createSyntheticExpression(left, leftType);
36503+
const syntheticCall = parseNodeFactory.createCallExpression(syntheticExpression, /*typeArguments*/ undefined, [syntheticArgument]);
36504+
setParent(syntheticCall, left.parent);
36505+
setTextRange(syntheticCall, left.parent);
36506+
return syntheticCall;
36507+
}
36508+
36509+
/**
36510+
* Tests whether a `CallExpression` is a synthetic call to a `[Symbol.hasInstance]` method as would be produced by
36511+
* {@link createSyntheticHasInstanceMethodCall}.
36512+
*/
36513+
function isSyntheticHasInstanceMethodCall(node: CallExpression) {
36514+
return isSyntheticExpression(node.expression) &&
36515+
node.arguments.length === 1 &&
36516+
isSyntheticExpression(node.arguments[0]) &&
36517+
isBinaryExpression(node.parent) &&
36518+
node.parent.operatorToken.kind === SyntaxKind.InstanceOfKeyword &&
36519+
node.parent.right === node.expression.parent &&
36520+
node.parent.left === node.arguments[0].parent;
36521+
}
36522+
3644836523
function checkInstanceOfExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type {
3644936524
if (leftType === silentNeverType || rightType === silentNeverType) {
3645036525
return silentNeverType;
@@ -36458,9 +36533,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3645836533
allTypesAssignableToKind(leftType, TypeFlags.Primitive)) {
3645936534
error(left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
3646036535
}
36461-
// NOTE: do not raise error if right is unknown as related error was already reported
36462-
if (!(isTypeAny(rightType) || typeHasCallOrConstructSignatures(rightType) || isTypeSubtypeOf(rightType, globalFunctionType))) {
36463-
error(right, Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_of_type_any_or_of_a_type_assignable_to_the_Function_interface_type);
36536+
if (!isTypeAny(rightType)) {
36537+
// if rightType is an object type with a custom `[Symbol.hasInstance]` method, then it is potentially
36538+
// valid on the right-hand side of the `instanceof` operator. This allows normal `object` types to
36539+
// 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);
36550+
}
36551+
// NOTE: do not raise error if right is unknown as related error was already reported
36552+
if (!(customHasInstanceMethodType || typeHasCallOrConstructSignatures(rightType) || isTypeSubtypeOf(rightType, globalFunctionType))) {
36553+
// 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 :
36558+
Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_of_type_any_or_of_a_type_assignable_to_the_Function_interface_type;
36559+
error(right, message);
36560+
}
3646436561
}
3646536562
return booleanType;
3646636563
}

src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -3659,6 +3659,18 @@
36593659
"category": "Error",
36603660
"code": 2854
36613661
},
3662+
"The left-hand side of an 'instanceof' expression must be assignable to the first argument of the right-hand side's '[Symbol.hasInstance]' method.": {
3663+
"category": "Error",
3664+
"code": 2855
3665+
},
3666+
"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.": {
3667+
"category": "Error",
3668+
"code": 2856
3669+
},
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.": {
3671+
"category": "Error",
3672+
"code": 2857
3673+
},
36623674

36633675
"Import declaration '{0}' is using private name '{1}'.": {
36643676
"category": "Error",

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2485,7 +2485,7 @@ export interface YieldExpression extends Expression {
24852485
readonly expression?: Expression;
24862486
}
24872487

2488-
export interface SyntheticExpression extends Expression {
2488+
export interface SyntheticExpression extends LeftHandSideExpression {
24892489
readonly kind: SyntaxKind.SyntheticExpression;
24902490
readonly isSpread: boolean;
24912491
readonly type: Type;

src/compiler/utilitiesPublic.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1978,6 +1978,7 @@ function isLeftHandSideExpressionKind(kind: SyntaxKind): boolean {
19781978
case SyntaxKind.MetaProperty:
19791979
case SyntaxKind.ImportKeyword: // technically this is only an Expression if it's in a CallExpression
19801980
case SyntaxKind.MissingDeclaration:
1981+
case SyntaxKind.SyntheticExpression: // synthetic expressions are only used by the checker to substitute specific types for expression positions, so their precedence does not matter.
19811982
return true;
19821983
default:
19831984
return false;

0 commit comments

Comments
 (0)