diff --git a/internal/checker/checker.go b/internal/checker/checker.go index a8223d2bac..bf549d211d 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 b60d9c9707..5e44a46828 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 { + 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) @@ -2420,19 +2427,62 @@ 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 { + } 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) { + leftType := c.checkExpression(objectExpr) + objectType = c.GetNonNullableType(c.getOptionalExpressionType(leftType, objectExpr)) + } else { + objectType = c.GetNonNullableType(c.checkExpression(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) + 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 { 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 0000000000..3a5788f096 --- /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 0000000000..f53d5f453a --- /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 0000000000..d1d097c01c --- /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 +}