-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Fix logic for determining whether to simplify keyof on mapped types #44042
Conversation
src/compiler/checker.ts
Outdated
return type.flags & TypeFlags.TypeParameter ? type === typeVariable : | ||
type.flags & TypeFlags.Conditional ? (<ConditionalType>type).root.isDistributive && isDistributiveForTypeParameter((<ConditionalType>type).checkType) : | ||
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) ? every((<UnionOrIntersectionType | TemplateLiteralType>type).types, isDistributiveForTypeParameter) : | ||
type.flags & TypeFlags.IndexedAccess ? isDistributiveForTypeParameter((<IndexedAccessType>type).indexType) : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't the object type also essentially pass thru distrubutivity? Since (A | B)["x"]
is identical to A["x"] | B["x"]
(for reading, at least)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that exactly what this does? It completely ignores the object type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With latest commit we now check both objectType
and indexType
.
src/compiler/checker.ts
Outdated
type.flags & TypeFlags.Conditional ? (<ConditionalType>type).root.isDistributive && isDistributiveForTypeParameter((<ConditionalType>type).checkType) : | ||
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) ? every((<UnionOrIntersectionType | TemplateLiteralType>type).types, isDistributiveForTypeParameter) : | ||
type.flags & TypeFlags.IndexedAccess ? isDistributiveForTypeParameter((<IndexedAccessType>type).indexType) : | ||
type.flags & TypeFlags.Substitution ? isDistributiveForTypeParameter((<SubstitutionType>type).substitute) : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The substitute may erase the original type parameter at the position (eg, because it mixed in any
), so it may be pertinent to check for either the substitute or the base type containing a match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessary with the latest changes.
src/compiler/checker.ts
Outdated
type.flags & TypeFlags.IndexedAccess ? isDistributiveForTypeParameter((<IndexedAccessType>type).indexType) : | ||
type.flags & TypeFlags.Substitution ? isDistributiveForTypeParameter((<SubstitutionType>type).substitute) : | ||
type.flags & TypeFlags.StringMapping ? isDistributiveForTypeParameter((<StringMappingType>type).type) : | ||
type.flags & TypeFlags.Index ? false : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about, eg, keyof {[_ in T]: whatever}
? That's still essentially distributive over T since it should just be T
(T just has to be constrained to valid key types).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that handled by getNameTypeFromMappedType(mappedType) || typeVariable
in the original call to isDistributiveForTypeParameter
? If there's no NameType, typeVariable
will immediately pass the check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A keyof { [_ in T]: xxx }
will, if possible, already have been simplified by getIndexType
.
Some background on the change in this PR... For a type With that in mind, the best approach is to not simplify unless we can definitely prove a type to be distributive. Originally the code was doing it the other way around (assuming distributive and attempting to prove otherwise), but that meant we'd get it wrong in cases where the analysis was incomplete. |
src/compiler/checker.ts
Outdated
const typeVariable = getTypeParameterFromMappedType(mappedType); | ||
return isDistributiveForTypeParameter(getNameTypeFromMappedType(mappedType) || typeVariable); | ||
function isDistributiveForTypeParameter(type: Type): boolean { | ||
return type.flags & TypeFlags.TypeParameter ? type === typeVariable : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a bug per se, but isn't this check too strict? eg it causes
type NotDistributive<T, U, V = string> = keyof { [ K in keyof T as V & (K extends U ? K : never)]: T[K]};
to fail the isDistributiveForTypeParameter
, because V
is rejected by the every
call.
Maybe add a flag to isDistributiveForTypeParameter
saying whether or not you're under a checkType, default it to false (for the initial call, and the every
call), pass true for the checkType call, and pass it through for the other cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, fixed in the latest commit.
src/compiler/checker.ts
Outdated
return type.flags & TypeFlags.TypeParameter ? type === typeVariable : | ||
type.flags & TypeFlags.Conditional ? (<ConditionalType>type).root.isDistributive && isDistributiveForTypeParameter((<ConditionalType>type).checkType) : | ||
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) ? every((<UnionOrIntersectionType | TemplateLiteralType>type).types, isDistributiveForTypeParameter) : | ||
type.flags & TypeFlags.IndexedAccess ? isDistributiveForTypeParameter((<IndexedAccessType>type).indexType) : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that exactly what this does? It completely ignores the object type.
src/compiler/checker.ts
Outdated
type.flags & TypeFlags.IndexedAccess ? isDistributiveForTypeParameter((<IndexedAccessType>type).indexType) : | ||
type.flags & TypeFlags.Substitution ? isDistributiveForTypeParameter((<SubstitutionType>type).substitute) : | ||
type.flags & TypeFlags.StringMapping ? isDistributiveForTypeParameter((<StringMappingType>type).type) : | ||
type.flags & TypeFlags.Index ? false : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that handled by getNameTypeFromMappedType(mappedType) || typeVariable
in the original call to isDistributiveForTypeParameter
? If there's no NameType, typeVariable
will immediately pass the check.
# Conflicts: # src/compiler/checker.ts
type.flags & TypeFlags.IndexedAccess ? isDistributive((type as IndexedAccessType).objectType) && isDistributive((type as IndexedAccessType).indexType) : | ||
type.flags & TypeFlags.Substitution ? isDistributive((type as SubstitutionType).substitute) : | ||
type.flags & TypeFlags.StringMapping ? isDistributive((type as StringMappingType).type) : | ||
true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your background comment says With that in mind, the best approach is to not simplify unless we can definitely prove a type to be distributive, so shouldn't this true
be false
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, another test case [edit: linked to the correct test case]. Again, it should reject the call to f, but fails to do so in the playground, and fails to do so after this fix.
Changing line 14349 to !(type.flags & TypeFlags.Index)
fixes this particular test case, but if we're trying to be conservative false
would be better. Or maybe !!(type.flags & TypeFlags.Primitive)
to avoid being overly conservative [edit: more flags would need to be checked; eg at least TypeFlags.TypeParameter
to avoid being way too conservative]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, not sure what's going on with the playground links - they seem to randomly link to other test cases I've been playing with. In case its behaving in a similar way for you, here's the code I'm trying to link to:
type KeyofMapped<T, U> = keyof { [ K in keyof T as keyof ({[P in K as T[P] extends U ? K : never]: true}) ]: T[K] };
interface M {
a: boolean;
b: number;
}
function f(x: KeyofMapped<M, boolean>) {
return x;
}
f("b");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With latest commits now errors as expected.
} | ||
function isDistributiveCheckType(type: Type): boolean { | ||
return !!(type.flags & TypeFlags.TypeParameter) && type === typeVariable || | ||
!!(type.flags & TypeFlags.Conditional) && (type as ConditionalType).root.isDistributive && isDistributiveCheckType((type as ConditionalType).checkType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this be problematic?
type Dist<U, V, W, X> = U extends V ? W : X;
type KeyofMapped<T, U, V> = keyof { [ K in keyof T as Dist<K extends U ? T[K] : never, T[K], K, never> ]: T[K] };
Now, we hit this "inner, distributive conditional" case, and the inner conditional is distributive wrt K
, so we do the transformation. When its finally instantiated, the inner conditional correctly expands to a union of those fields of T specified by U. So the checkType of the outer conditional is a union, and we distribute that too; but now we're distributing something that isn't actually K. So as long as at least one field was picked by the inner conditional, we end up choosing all the keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should reject the call to f, but accepts it instead. The playground accepts it, and this fix also accepts it. If I remove line 14353 from your fix, it correctly rejects it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, here's the code I'm trying to link to for this example:
type Dist<U, V, W, X> = U extends V ? W : X;
type KeyofMapped<T, U> = keyof { [ K in keyof T as Dist<K extends U ? T[K] : never, T[K], K, never> ]: T[K] };
interface M {
a: boolean;
b: number;
}
function f(x : KeyofMapped<M, "a">) {
return x;
}
f("b");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, with latest commits now errors as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a higher level question: why do this transformation at all?
If it's done correctly (and we seem to be seeing that that's actually quite hard), it won't change the behavior, so it doesn't appear to be necessary. Is it supposed to be speeding up the compilation step by simplifying the expression? In that case, its not clear to me that it's a win.
Typically I will have
type MakeMappedType<Args> = { ... };
type KeyofMappedType<Args> = keyof MakeMappedType<Args>;
And then I will always use them in pairs - ie for every set of Args that I instantiate KeyofMappedType, I will also instantiate MakeMappedType (or I won't even bother with KeyofMappedType, and just use keyof MakeMappedType
directly). Assuming my usage model is typical, there doesn't seem to be a win, since without the transformation, keyof MakeMappedType
would just read the property keys from the already-instantiated mapped type.
@markw65 Thanks for the latest tests which should now work as expected. WRT why we do this transformation in the first place, the short answer is: In order to ensure types that are expected to be compatible actually are compatible. For example type T1<T> = keyof T & string;
type T2<T> = keyof { [P in keyof T as P & string]: number }; You'd expect these two types to be compatible since they'll always have the same sets of properties, but they wouldn't be without the simplification. |
@ahejlsberg Sorry, would you mind clarifying that? Maybe with an example, or an explanation of what I'm doing wrong here:
In the above code, If I change the definition of |
@markw65 It works because function foo1(x: T1<M>, y: T2<M>) {
x = y; // Ok
y = x; // Ok
}
function foo2<M>(x: T1<M>, y: T2<M>) {
x = y; // Error in non-simplified case
y = x; // Error in non-simplified case
} |
@ahejlsberg Thanks. That makes sense. But in that case, it seems like its not really ok to be conservative about when to do the transformation? eg right now it would reject
even though it is distributive wrt K. [edit: never mind about the last part, it is a bug, and it's fixed in the nightly. Sorry should have checked that first]. Can you explain what's going on with f3's parameter type? We know that
|
Fixes #44019.