Skip to content

Narrowed type not used when indexing #55922

Closed
@gabritto

Description

@gabritto

πŸ”Ž 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);
}

Activity

DanielRosenwasser

DanielRosenwasser commented on Oct 1, 2023

@DanielRosenwasser
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

Andarist commented on Oct 2, 2023

@Andarist
Contributor

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

ahejlsberg commented on Oct 2, 2023

@ahejlsberg
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

gabritto commented on Oct 3, 2023

@gabritto
MemberAuthor

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?

added
Design LimitationConstraints of the existing architecture prevent this from being fixed
and removed
Needs InvestigationThis issue needs a team member to investigate its status.
BugA bug in TypeScript
on Oct 3, 2023
removed their assignment
on Oct 3, 2023
ahejlsberg

ahejlsberg commented on Oct 5, 2023

@ahejlsberg
Member

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @DanielRosenwasser@ahejlsberg@RyanCavanaugh@Andarist@gabritto

        Issue actions

          Narrowed type not used when indexing Β· Issue #55922 Β· microsoft/TypeScript