Closed
Description
π 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
π» 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);
}
Metadata
Metadata
Assignees
Type
Projects
Relationships
Development
No branches or pull requests
Activity
DanielRosenwasser commentedon Oct 1, 2023
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 commentedon 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 throughgetNarrowableTypeForReference
andhasContextualTypeWithNoGenericTypes
. 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 commentedon Oct 2, 2023
@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 typeT
, the type predicateisString(x)
narrows the type ofx
toT & string
in the true case, reflecting the fact thatx
is both aT
and astring
. In the false case the type simply remainsT
, so if and when the true and false control flows join, the unionT & string | T
simply reduces back toT
. Since we don't have negated types, we don't have the option to narrow toT & 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 ofobj
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 likeT[K]
. This analysis becomes circular when both expressions are considered constraint positions.So, all up, this issue is a design limitation.
gabritto commentedon 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
?ahejlsberg commentedon Oct 5, 2023
For a type variable
T extends A | B | C
, following anx is A
type predicate test we currently narrow toT & A
in the true case. You could imagine us also narrowing toT & 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 asT & A | T & B | T & C
back to justT
such that we end up with the same type when control flows join. Implementing that logic in a performant manner is non-trivial.