Skip to content
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

Narrowed type not used when indexing #55922

Closed
gabritto opened this issue Sep 29, 2023 · 5 comments
Closed

Narrowed type not used when indexing #55922

gabritto opened this issue Sep 29, 2023 · 5 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@gabritto
Copy link
Member

gabritto commented Sep 29, 2023

πŸ”Ž Search Terms

"cannot be used to index type"

πŸ•— Version & Regression Information

  • This is the behavior in every version since 4.3. Before 4.3, CFA doesn't work.

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.3.0-dev.20230929#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXywGcAlEAcwFEAPABwAoqAueKVATwEpmqDD5TKtANwAoUJFgIweQhniEcAWxAB5AEYArZgG94AbULNZMLKjIBdZmpw4IIVvAC+okSnTY8SGwB4AkvBAqDBBUYD5jUzJ4AB9+cmoaAD4GZl8OeG0ReAJEeDoiAQSGDnTM7OyMKABrEBJ42mLRcvg4DGQYVCanLPhKmsIAZQwTM0b4AHpx+AB1BCrUHAB3eCZ5Ycj4AAsQOB6FZXUNPSpzIQmpgCFkOVksCAh4RYRgPAByOSh7pfg2HGRenDwZCEBA8RA4GAEUKBSIiRwiMTgaBwJBoTC4fBEIYjMgpFjsLgrXhrHEuNzozzgnAAJj8ASCITCJI2sUKtGSqzSGR6WFy+UG61GVBK3OafVq2MijR62X2qk0x1O53gABVNkRHhCqoQZS0QG0Ol14RVqrU2fRhaJ4YiJCjyR58OKBTi6IZmWZCQA3HBYYCicTIhD2jG9U11QT0N3mr0+v1AA

πŸ’» Code

declare function takesString(s: string): void;
declare function takesRegExp(s: RegExp): void;

declare function isRegExp(x: any): x is RegExp;
declare const someObj: { [s: string]: boolean };

function foo<I extends string | RegExp>(x: I) {
  if (isRegExp(x)) {
    takesRegExp(x);
    return;
  }
  takesString(x); // We know x: string here
  someObj[x]; // But still we don't allow you to use x for indexing
}

πŸ™ Actual behavior

Error on someObj[x]: "Type 'I' cannot be used to index type '{ [s: string]: boolean; }'."

πŸ™‚ Expected behavior

No error: from CFA we know the type of x is actually constrained to string.

Additional information about the issue

If we reverse the order of the checks, i.e. check if the type of x is string in the if, the indexing works:

function foo2<I extends string | RegExp>(x: I) {
  if (isString(x)) {
    takesString(x);
    someObj[x]; // This works
    return;
  }
  takesRegExp(x);
}
@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Oct 1, 2023
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 5.3.1 milestone Oct 1, 2023
@DanielRosenwasser DanielRosenwasser added the Needs Investigation This issue needs a team member to investigate its status. label Oct 1, 2023
@DanielRosenwasser
Copy link
Member

There's a weird subtlety here where a type predicate returning true can guarantee something is true, but its false case can't guarantee something isn't false; but I always forget how much we do (or don't) abide by that. So you'll need to check in with @RyanCavanaugh.

@Andarist
Copy link
Contributor

Andarist commented Oct 2, 2023

Within the truthy branch, the compiler is able to create an intersection of the input type and the one checked by the predicate. That's not possible within the falsy branch because there is no way to express a negated type.

But why the function call works you might ask. That's because in that case we also have a contextual type and checkIdentifier consults it through getNarrowableTypeForReference and hasContextualTypeWithNoGenericTypes. Based on that, type (I) is being "downgraded" to its base constraint and that's being nicely narrowed down later through the existing CFA logic.

In the element access expression case, there is no contextual type and thus all of this doesn't happen. The type stays as I and that isn't narrowed down later on.

@ahejlsberg
Copy link
Member

@Andarist covered most of it, but since I was already writing this up...

When narrowing by a type predicate, when neither the current nor the candidate type is a subtype of the other (which typically is the case when the current type is generic), we create an intersection in the true case and leave the type unchanged in the false case. For example, for a variable x of a generic type T, the type predicate isString(x) narrows the type of x to T & string in the true case, reflecting the fact that x is both a T and a string. In the false case the type simply remains T, so if and when the true and false control flows join, the union T & string | T simply reduces back to T. Since we don't have negated types, we don't have the option to narrow to T & not string in the false case.

In #43183 we somewhat alleviate the lack of narrowing in the false case. However, in the expression obj[x] we only consider the object expression to be in a "constraint position", so only the type of obj is subject to the narrowing-by-constraint logic in #43183. In principle we could also consider the index expression to be in a constraint position, except it's complicated by the fact that we want to preserve generics when both the object and index types are generic such that we get an indexed access type like T[K]. This analysis becomes circular when both expressions are considered constraint positions.

So, all up, this issue is a design limitation.

@gabritto
Copy link
Member Author

gabritto commented Oct 3, 2023

Ok, I understand what's happening now, I think. I know we can't express the intersection for the negative/false case, but couldn't we do something like filtering the constraints of T?

@gabritto gabritto added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Needs Investigation This issue needs a team member to investigate its status. Bug A bug in TypeScript labels Oct 3, 2023
@gabritto gabritto removed their assignment Oct 3, 2023
@ahejlsberg
Copy link
Member

ahejlsberg commented Oct 5, 2023

couldn't we do something like filtering the constraints of T?

For a type variable T extends A | B | C, following an x is A type predicate test we currently narrow to T & A in the true case. You could imagine us also narrowing to T & B | T & C in the false case, but there are two issues with that approach. First issue is that for a large union constraint, we'd generate an equally large union of intersections with all but the single (typically) type that was checked for. That leads to unwieldy and expensive types. Second issue is that we'd need logic to reduce types such as T & A | T & B | T & C back to just T such that we end up with the same type when control flows join. Implementing that logic in a performant manner is non-trivial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

5 participants