Skip to content

Support custom 'Symbol.hasInstance' methods when narrowing 'instanceof' #55052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 137 additions & 18 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1920,7 +1920,7 @@
"category": "Error",
"code": 2358
},
"The right-hand side of an 'instanceof' expression must be of type 'any' or of a type assignable to the 'Function' interface type.": {
"The right-hand side of an 'instanceof' expression must be either of type 'any', a class, function, or other type assignable to the 'Function' interface type, or an object type with a 'Symbol.hasInstance' method.": {
"category": "Error",
"code": 2359
},
Expand Down Expand Up @@ -3691,6 +3691,14 @@
"category": "Error",
"code": 2859
},
"The left-hand side of an 'instanceof' expression must be assignable to the first argument of the right-hand side's '[Symbol.hasInstance]' method.": {
"category": "Error",
"code": 2860
},
"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.": {
"category": "Error",
"code": 2861
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3053,12 +3053,17 @@ export interface TaggedTemplateExpression extends MemberExpression {
/** @internal */ questionDotToken?: QuestionDotToken; // NOTE: Invalid syntax, only used to report a grammar error.
}

export interface InstanceofExpression extends BinaryExpression {
readonly operatorToken: Token<SyntaxKind.InstanceOfKeyword>;
}

export type CallLikeExpression =
| CallExpression
| NewExpression
| TaggedTemplateExpression
| Decorator
| JsxOpeningLikeElement;
| JsxOpeningLikeElement
| InstanceofExpression;

export interface AsExpression extends Expression {
readonly kind: SyntaxKind.AsExpression;
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ import {
IndexSignatureDeclaration,
InitializedVariableDeclaration,
insertSorted,
InstanceofExpression,
InterfaceDeclaration,
InternalEmitFlags,
isAccessor,
Expand Down Expand Up @@ -3120,6 +3121,8 @@ export function getInvokedExpression(node: CallLikeExpression): Expression | Jsx
case SyntaxKind.JsxOpeningElement:
case SyntaxKind.JsxSelfClosingElement:
return node.tagName;
case SyntaxKind.BinaryExpression:
return node.right;
default:
return node.expression;
}
Expand Down Expand Up @@ -7235,6 +7238,15 @@ export function isRightSideOfQualifiedNameOrPropertyAccessOrJSDocMemberName(node
|| isPropertyAccessExpression(node.parent) && node.parent.name === node
|| isJSDocMemberName(node.parent) && node.parent.right === node;
}
/** @internal */
export function isInstanceOfExpression(node: Node): node is InstanceofExpression {
return isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.InstanceOfKeyword;
}

/** @internal */
export function isRightSideOfInstanceofExpression(node: Node) {
return isInstanceOfExpression(node.parent) && node === node.parent.right;
}

/** @internal */
export function isEmptyObjectLiteral(expression: Node): boolean {
Expand Down
5 changes: 4 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5646,7 +5646,10 @@ declare namespace ts {
readonly typeArguments?: NodeArray<TypeNode>;
readonly template: TemplateLiteral;
}
type CallLikeExpression = CallExpression | NewExpression | TaggedTemplateExpression | Decorator | JsxOpeningLikeElement;
interface InstanceofExpression extends BinaryExpression {
readonly operatorToken: Token<SyntaxKind.InstanceOfKeyword>;
}
type CallLikeExpression = CallExpression | NewExpression | TaggedTemplateExpression | Decorator | JsxOpeningLikeElement | InstanceofExpression;
interface AsExpression extends Expression {
readonly kind: SyntaxKind.AsExpression;
readonly expression: Expression;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
//// [tests/cases/compiler/controlFlowInstanceofWithSymbolHasInstance.ts] ////

=== controlFlowInstanceofWithSymbolHasInstance.ts ===
interface PromiseConstructor {
[Symbol.hasInstance](value: any): value is Promise<any>;
>[Symbol.hasInstance] : (value: any) => value is Promise<any>
>Symbol.hasInstance : unique symbol
>Symbol : SymbolConstructor
>hasInstance : unique symbol
>value : any
}

interface SetConstructor {
[Symbol.hasInstance](value: any): value is Set<any>;
>[Symbol.hasInstance] : (value: any) => value is Set<any>
>Symbol.hasInstance : unique symbol
>Symbol : SymbolConstructor
>hasInstance : unique symbol
>value : any
}

function f1(s: Set<string> | Set<number>) {
>f1 : (s: Set<string> | Set<number>) => void
>s : Set<string> | Set<number>

s = new Set<number>();
>s = new Set<number>() : Set<number>
>s : Set<string> | Set<number>
>new Set<number>() : Set<number>
>Set : SetConstructor

s; // Set<number>
>s : Set<number>

if (s instanceof Set) {
>s instanceof Set : boolean
>s : Set<number>
>Set : SetConstructor

s; // Set<number>
>s : Set<number>
}
s; // Set<number>
>s : Set<number>

s.add(42);
>s.add(42) : Set<number>
>s.add : (value: number) => Set<number>
>s : Set<number>
>add : (value: number) => Set<number>
>42 : 42
}

function f2(s: Set<string> | Set<number>) {
>f2 : (s: Set<string> | Set<number>) => void
>s : Set<string> | Set<number>

s = new Set<number>();
>s = new Set<number>() : Set<number>
>s : Set<string> | Set<number>
>new Set<number>() : Set<number>
>Set : SetConstructor

s; // Set<number>
>s : Set<number>

if (s instanceof Promise) {
>s instanceof Promise : boolean
>s : Set<number>
>Promise : PromiseConstructor

s; // Set<number> & Promise<any>
>s : Set<number> & Promise<any>
}
s; // Set<number>
>s : Set<number>

s.add(42);
>s.add(42) : Set<number>
>s.add : (value: number) => Set<number>
>s : Set<number>
>add : (value: number) => Set<number>
>42 : 42
}

function f3(s: Set<string> | Set<number>) {
>f3 : (s: Set<string> | Set<number>) => void
>s : Set<string> | Set<number>

s; // Set<string> | Set<number>
>s : Set<string> | Set<number>

if (s instanceof Set) {
>s instanceof Set : boolean
>s : Set<string> | Set<number>
>Set : SetConstructor

s; // Set<string> | Set<number>
>s : Set<string> | Set<number>
}
else {
s; // never
>s : never
}
}

function f4(s: Set<string> | Set<number>) {
>f4 : (s: Set<string> | Set<number>) => void
>s : Set<string> | Set<number>

s = new Set<number>();
>s = new Set<number>() : Set<number>
>s : Set<string> | Set<number>
>new Set<number>() : Set<number>
>Set : SetConstructor

s; // Set<number>
>s : Set<number>

if (s instanceof Set) {
>s instanceof Set : boolean
>s : Set<number>
>Set : SetConstructor

s; // Set<number>
>s : Set<number>
}
else {
s; // never
>s : never
}
}

// More tests

class A {
>A : A

a: string;
>a : string

static [Symbol.hasInstance]<T>(this: T, value: unknown): value is (
>[Symbol.hasInstance] : <T>(this: T, value: unknown) => value is T extends abstract new (...args: any) => infer U ? U : never
>Symbol.hasInstance : unique symbol
>Symbol : SymbolConstructor
>hasInstance : unique symbol
>this : T
>value : unknown

T extends (abstract new (...args: any) => infer U) ? U :
>args : any

never
) {
return Function.prototype[Symbol.hasInstance].call(this, value);
>Function.prototype[Symbol.hasInstance].call(this, value) : any
>Function.prototype[Symbol.hasInstance].call : (this: Function, thisArg: any, ...argArray: any[]) => any
>Function.prototype[Symbol.hasInstance] : (value: any) => boolean
>Function.prototype : Function
>Function : FunctionConstructor
>prototype : Function
>Symbol.hasInstance : unique symbol
>Symbol : SymbolConstructor
>hasInstance : unique symbol
>call : (this: Function, thisArg: any, ...argArray: any[]) => any
>this : T
>value : unknown
}
}
class B extends A { b: string }
>B : B
>A : A
>b : string

class C extends A { c: string }
>C : C
>A : A
>c : string

function foo(x: A | undefined) {
>foo : (x: A | undefined) => void
>x : A | undefined

x; // A | undefined
>x : A | undefined

if (x instanceof B || x instanceof C) {
>x instanceof B || x instanceof C : boolean
>x instanceof B : boolean
>x : A | undefined
>B : typeof B
>x instanceof C : boolean
>x : A | undefined
>C : typeof C

x; // B | C
>x : B | C
}
x; // A | undefined
>x : A | undefined

if (x instanceof B && x instanceof C) {
>x instanceof B && x instanceof C : boolean
>x instanceof B : boolean
>x : A | undefined
>B : typeof B
>x instanceof C : boolean
>x : B
>C : typeof C

x; // B & C
>x : B & C
}
x; // A | undefined
>x : A | undefined

if (!x) {
>!x : boolean
>x : A | undefined

return;
}
x; // A
>x : A

if (x instanceof B) {
>x instanceof B : boolean
>x : A
>B : typeof B

x; // B
>x : B

if (x instanceof C) {
>x instanceof C : boolean
>x : B
>C : typeof C

x; // B & C
>x : B & C
}
else {
x; // B
>x : B
}
x; // B
>x : B
}
else {
x; // A
>x : A
}
x; // A
>x : A
}

// X is neither assignable to Y nor a subtype of Y
// Y is assignable to X, but not a subtype of X

interface X {
x?: string;
>x : string | undefined
}

class Y {
>Y : Y

y: string;
>y : string
}

function goo(x: X) {
>goo : (x: X) => void
>x : X

x;
>x : X

if (x instanceof Y) {
>x instanceof Y : boolean
>x : X
>Y : typeof Y

x.y;
>x.y : string
>x : X & Y
>y : string
}
x;
>x : X
}


Loading