From fcbae3457307ad10dd6ef093a1fa49f0b162a402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=92=D0=BE=D0=BB?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2?= Date: Tue, 23 Jun 2026 14:56:38 +0300 Subject: [PATCH 1/2] feat(feature): add type guard getter support with this-type predicates --- internal/checker/checker.go | 2 +- internal/checker/flow.go | 45 ++++++++++- .../typeGuardGetterTypePredicate.symbols | 71 +++++++++++++++++ .../typeGuardGetterTypePredicate.types | 78 +++++++++++++++++++ .../compiler/typeGuardGetterTypePredicate.ts | 28 +++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.symbols create mode 100644 testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.types create mode 100644 testdata/tests/cases/compiler/typeGuardGetterTypePredicate.ts diff --git a/internal/checker/checker.go b/internal/checker/checker.go index a8223d2bac9..bf549d211d0 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -3084,7 +3084,7 @@ func (c *Checker) getTypePredicateParent(node *ast.Node) *ast.SignatureDeclarati parent := node.Parent switch parent.Kind { case ast.KindArrowFunction, ast.KindCallSignature, ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindFunctionType, - ast.KindMethodDeclaration, ast.KindMethodSignature: + ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindGetAccessor: if node == parent.Type() { return parent } diff --git a/internal/checker/flow.go b/internal/checker/flow.go index b60d9c97075..840448fba92 100644 --- a/internal/checker/flow.go +++ b/internal/checker/flow.go @@ -397,6 +397,13 @@ func (c *Checker) narrowType(f *FlowState, t *Type, expr *ast.Node, assumeTrue b } fallthrough case ast.KindThisKeyword, ast.KindSuperKeyword, ast.KindPropertyAccessExpression, ast.KindElementAccessExpression: + if predicate := c.getTypePredicateOfPropertyAccess(expr); predicate != nil && (predicate.kind == TypePredicateKindThis || predicate.kind == TypePredicateKindIdentifier) { + if c.isOrContainsMatchingReference(f.reference, expr.Expression()) { + if narrowedType := c.narrowTypeByTypePredicate(f, t, predicate, expr, assumeTrue); narrowedType != t { + return narrowedType + } + } + } return c.narrowTypeByTruthiness(f, t, expr, assumeTrue) case ast.KindCallExpression: return c.narrowTypeByCallExpression(f, t, expr, assumeTrue) @@ -2424,15 +2431,51 @@ func (c *Checker) getTypePredicateArgument(predicate *TypePredicate, callExpress if predicate.parameterIndex >= 0 && int(predicate.parameterIndex) < len(arguments) { return arguments[predicate.parameterIndex] } - } else { + } else if ast.IsCallExpression(callExpression) { invokedExpression := ast.SkipParentheses(callExpression.Expression()) if ast.IsAccessExpression(invokedExpression) { return ast.SkipParentheses(invokedExpression.Expression()) } + } else if ast.IsPropertyAccessExpression(callExpression) || ast.IsElementAccessExpression(callExpression) { + return ast.SkipParentheses(callExpression.Expression()) } return nil } +func (c *Checker) getTypePredicateOfPropertyAccess(node *ast.Node) *TypePredicate { + if !ast.IsPropertyAccessExpression(node) && !ast.IsElementAccessExpression(node) { + return nil + } + objectExpr := node.Expression() + if objectExpr.Kind == ast.KindSuperKeyword { + return nil + } + var objectType *Type + if ast.IsOptionalChain(node) { + objectType = c.checkNonNullType(c.getOptionalExpressionType(c.checkExpression(objectExpr), objectExpr), objectExpr) + } else { + objectType = c.checkNonNullExpression(objectExpr) + } + propName, ok := c.getAccessedPropertyName(node) + if !ok { + return nil + } + prop := c.getPropertyOfType(objectType, propName) + if prop == nil || prop.Flags&ast.SymbolFlagsGetAccessor == 0 { + return nil + } + getter := ast.GetDeclarationOfKind(prop, ast.KindGetAccessor) + if getter == nil { + return nil + } + typeNode := getter.Type() + if typeNode == nil || !ast.IsTypePredicateNode(typeNode) { + return nil + } + sig := c.getSignatureFromDeclaration(getter) + return c.getTypePredicateOfSignature(sig) +} + func (c *Checker) getFlowTypeInConstructor(symbol *ast.Symbol, constructor *ast.Node) *Type { var accessName *ast.Node if strings.HasPrefix(symbol.Name, ast.InternalSymbolNamePrefix+"#") { diff --git a/testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.symbols b/testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.symbols new file mode 100644 index 00000000000..3a5788f096e --- /dev/null +++ b/testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.symbols @@ -0,0 +1,71 @@ +//// [tests/cases/compiler/typeGuardGetterTypePredicate.ts] //// + +=== typeGuardGetterTypePredicate.ts === +class Session { +>Session : Symbol(Session, Decl(typeGuardGetterTypePredicate.ts, 0, 0)) + + user: string | null = null; +>user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) + + get hasUser(): this is { user: string } { +>hasUser : Symbol(Session.hasUser, Decl(typeGuardGetterTypePredicate.ts, 1, 31)) +>user : Symbol(user, Decl(typeGuardGetterTypePredicate.ts, 3, 28)) + + return this.user !== null; +>this.user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) +>this : Symbol(Session, Decl(typeGuardGetterTypePredicate.ts, 0, 0)) +>user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) + } + + hasUserMethod(): this is { user: string } { +>hasUserMethod : Symbol(Session.hasUserMethod, Decl(typeGuardGetterTypePredicate.ts, 5, 5)) +>user : Symbol(user, Decl(typeGuardGetterTypePredicate.ts, 7, 30)) + + return this.user !== null; +>this.user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) +>this : Symbol(Session, Decl(typeGuardGetterTypePredicate.ts, 0, 0)) +>user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) + } +} + +const session = new Session(); +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>Session : Symbol(Session, Decl(typeGuardGetterTypePredicate.ts, 0, 0)) + +if (session.hasUser) { +>session.hasUser : Symbol(Session.hasUser, Decl(typeGuardGetterTypePredicate.ts, 1, 31)) +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>hasUser : Symbol(Session.hasUser, Decl(typeGuardGetterTypePredicate.ts, 1, 31)) + + session.user.toUpperCase(); +>session.user.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>session.user : Symbol(user, Decl(typeGuardGetterTypePredicate.ts, 0, 15), Decl(typeGuardGetterTypePredicate.ts, 3, 28)) +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>user : Symbol(user, Decl(typeGuardGetterTypePredicate.ts, 0, 15), Decl(typeGuardGetterTypePredicate.ts, 3, 28)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +} + +if (session.hasUserMethod()) { +>session.hasUserMethod : Symbol(Session.hasUserMethod, Decl(typeGuardGetterTypePredicate.ts, 5, 5)) +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>hasUserMethod : Symbol(Session.hasUserMethod, Decl(typeGuardGetterTypePredicate.ts, 5, 5)) + + session.user.toUpperCase(); +>session.user.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>session.user : Symbol(user, Decl(typeGuardGetterTypePredicate.ts, 0, 15), Decl(typeGuardGetterTypePredicate.ts, 7, 30)) +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>user : Symbol(user, Decl(typeGuardGetterTypePredicate.ts, 0, 15), Decl(typeGuardGetterTypePredicate.ts, 7, 30)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +} + +if (!session.hasUser) { +>session.hasUser : Symbol(Session.hasUser, Decl(typeGuardGetterTypePredicate.ts, 1, 31)) +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>hasUser : Symbol(Session.hasUser, Decl(typeGuardGetterTypePredicate.ts, 1, 31)) + + session.user; // string | null +>session.user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) +>session : Symbol(session, Decl(typeGuardGetterTypePredicate.ts, 12, 5)) +>user : Symbol(Session.user, Decl(typeGuardGetterTypePredicate.ts, 0, 15)) +} + diff --git a/testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.types b/testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.types new file mode 100644 index 00000000000..f53d5f453ab --- /dev/null +++ b/testdata/baselines/reference/compiler/typeGuardGetterTypePredicate.types @@ -0,0 +1,78 @@ +//// [tests/cases/compiler/typeGuardGetterTypePredicate.ts] //// + +=== typeGuardGetterTypePredicate.ts === +class Session { +>Session : Session + + user: string | null = null; +>user : string | null + + get hasUser(): this is { user: string } { +>hasUser : boolean +>user : string + + return this.user !== null; +>this.user !== null : boolean +>this.user : string | null +>this : this +>user : string | null + } + + hasUserMethod(): this is { user: string } { +>hasUserMethod : () => this is { user: string; } +>user : string + + return this.user !== null; +>this.user !== null : boolean +>this.user : string | null +>this : this +>user : string | null + } +} + +const session = new Session(); +>session : Session +>new Session() : Session +>Session : typeof Session + +if (session.hasUser) { +>session.hasUser : boolean +>session : Session +>hasUser : boolean + + session.user.toUpperCase(); +>session.user.toUpperCase() : string +>session.user.toUpperCase : () => string +>session.user : string +>session : Session & { user: string; } +>user : string +>toUpperCase : () => string +} + +if (session.hasUserMethod()) { +>session.hasUserMethod() : boolean +>session.hasUserMethod : () => this is { user: string; } +>session : Session +>hasUserMethod : () => this is { user: string; } + + session.user.toUpperCase(); +>session.user.toUpperCase() : string +>session.user.toUpperCase : () => string +>session.user : string +>session : Session & { user: string; } +>user : string +>toUpperCase : () => string +} + +if (!session.hasUser) { +>!session.hasUser : boolean +>session.hasUser : boolean +>session : Session +>hasUser : boolean + + session.user; // string | null +>session.user : string | null +>session : Session +>user : string | null +} + diff --git a/testdata/tests/cases/compiler/typeGuardGetterTypePredicate.ts b/testdata/tests/cases/compiler/typeGuardGetterTypePredicate.ts new file mode 100644 index 00000000000..d1d097c01cc --- /dev/null +++ b/testdata/tests/cases/compiler/typeGuardGetterTypePredicate.ts @@ -0,0 +1,28 @@ +// @strict: true +// @noEmit: true + +class Session { + user: string | null = null; + + get hasUser(): this is { user: string } { + return this.user !== null; + } + + hasUserMethod(): this is { user: string } { + return this.user !== null; + } +} + +const session = new Session(); + +if (session.hasUser) { + session.user.toUpperCase(); +} + +if (session.hasUserMethod()) { + session.user.toUpperCase(); +} + +if (!session.hasUser) { + session.user; // string | null +} From 760bab17b869746647d819bc8376d69177f0d854 Mon Sep 17 00:00:00 2001 From: Sergey Volkov Date: Tue, 23 Jun 2026 15:18:28 +0300 Subject: [PATCH 2/2] fix: comment at PR --- internal/checker/flow.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/internal/checker/flow.go b/internal/checker/flow.go index 840448fba92..5e44a468285 100644 --- a/internal/checker/flow.go +++ b/internal/checker/flow.go @@ -397,7 +397,7 @@ func (c *Checker) narrowType(f *FlowState, t *Type, expr *ast.Node, assumeTrue b } fallthrough case ast.KindThisKeyword, ast.KindSuperKeyword, ast.KindPropertyAccessExpression, ast.KindElementAccessExpression: - if predicate := c.getTypePredicateOfPropertyAccess(expr); predicate != nil && (predicate.kind == TypePredicateKindThis || predicate.kind == TypePredicateKindIdentifier) { + if predicate := c.getTypePredicateOfPropertyAccess(expr); predicate != nil && predicate.kind == TypePredicateKindThis { if c.isOrContainsMatchingReference(f.reference, expr.Expression()) { if narrowedType := c.narrowTypeByTypePredicate(f, t, predicate, expr, assumeTrue); narrowedType != t { return narrowedType @@ -2427,9 +2427,11 @@ func (c *Checker) typeMaybeAssignableTo(source *Type, target *Type) bool { func (c *Checker) getTypePredicateArgument(predicate *TypePredicate, callExpression *ast.Node) *ast.Node { if predicate.kind == TypePredicateKindIdentifier || predicate.kind == TypePredicateKindAssertsIdentifier { - arguments := callExpression.Arguments() - if predicate.parameterIndex >= 0 && int(predicate.parameterIndex) < len(arguments) { - return arguments[predicate.parameterIndex] + if ast.IsCallExpression(callExpression) || ast.IsNewExpression(callExpression) { + arguments := callExpression.Arguments() + if predicate.parameterIndex >= 0 && int(predicate.parameterIndex) < len(arguments) { + return arguments[predicate.parameterIndex] + } } } else if ast.IsCallExpression(callExpression) { invokedExpression := ast.SkipParentheses(callExpression.Expression()) @@ -2452,9 +2454,10 @@ func (c *Checker) getTypePredicateOfPropertyAccess(node *ast.Node) *TypePredicat } var objectType *Type if ast.IsOptionalChain(node) { - objectType = c.checkNonNullType(c.getOptionalExpressionType(c.checkExpression(objectExpr), objectExpr), objectExpr) + leftType := c.checkExpression(objectExpr) + objectType = c.GetNonNullableType(c.getOptionalExpressionType(leftType, objectExpr)) } else { - objectType = c.checkNonNullExpression(objectExpr) + objectType = c.GetNonNullableType(c.checkExpression(objectExpr)) } propName, ok := c.getAccessedPropertyName(node) if !ok { @@ -2473,7 +2476,11 @@ func (c *Checker) getTypePredicateOfPropertyAccess(node *ast.Node) *TypePredicat return nil } sig := c.getSignatureFromDeclaration(getter) - return c.getTypePredicateOfSignature(sig) + predicate := c.getTypePredicateOfSignature(sig) + if predicate == nil || predicate.kind != TypePredicateKindThis { + return nil + } + return predicate } func (c *Checker) getFlowTypeInConstructor(symbol *ast.Symbol, constructor *ast.Node) *Type {