You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
undefined extends all types when accessing a generic in a mapped typed #27470
This one is rather involved. We currently have the following rule:
A type S is related to a type T[K] if S is related to C, where C is the base constraint of T[K].
This rule is unsound because T[K] may be a subtype of C upon instantiation, but we nonetheless need it for several of reasons. One key reason is the fact that we don't use higher order types for property accesses through type variables. For example, for a property access expression this.xxx in a method of a class, we resolve to the declared type of xxx, not type this["xxx"], even though the latter is more precise. Effectively, we resolve to the constraint of this["xxx"]. We do this for reasons of backwards compatibility and also to avoid drowning in generic types. But it would be odd indeed if the expression this.xxx was not assignable to the type this["xxx"]--hence the rule that allows the assignment if it matches the constraint.
Now, in 3.1 we fixed several issues relating to us not examining all constraints of a type. Because of those fixes we now drill all the way down and find the any constraint for T[K] in your example above. And since anything is assignable to any we effectively turn off type checking. To wit, none of the following assignments generate errors:
That's not good. But the question is how to fix this without breaking the this.xxx scenario I mentioned above (and other similar ones).
So far I think the best contender is to modify the rule above as follows:
A type S is related to a type T[K], where T and K aren't both type variables, if S is related to C, where C is the base constraint of T[K].
In other words, we should limit the unsoundness to situations where one or the other part of the indexed access is an actual zero-order type, but forbid it when both are higher order (since at that point it becomes too unsound).
I ran the experiment on our test suites and there just a few changes, all of which are reasonable. Even better, the RWC suites show no baseline changes at all. I will put this up in a PR.
And since anything is assignable to any we effectively turn off type checking. To wit, none of the following assignments generate errors
FWIW I don't think it is because type-checking is turned off, but because any is a top type (I don't know if you consider this a distinction). You get the same behavior with unknown.
typeUndefinedKeys<TextendsRecord<string,unknown>>={[KinkeyofT]: undefinedextendsT[K] ? K : never};typeMyType={a: string,b: string|undefined}typeResult1=UndefinedKeys<MyType>;consta1: Result1['a']='a';// no error
The conditional type is essentially a type-level version of this:
(This isn't a complaint, just wanting to know if I read the rule ok).
That's not good. But the question is how to fix this without breaking the this.xxx scenario I mentioned above (and other similar ones).
From an uninformed/outside perspective it seems like the problem trying to be solved in the initial post is to constrain T to some shape (a record, but it could be an array). The programmer only really cares about the outermost data constructor and the any is just there because it causes little friction. Though when you pull down the constraints the any starts causing problems and you can't control the variance.
I wonder whether it would be possible to have a special way of marking shapes for types where you really don't care about, or particularly, want their constituent constraints. Something like:
typeUndefinedKeys<TextendsRecord<string,*>>={[KinkeyofT]: undefinedextendsT[K] ? K : never};
T extends Record<string, *> can be read as: T should be some record but other than that we don't care about the type of the value, please don't pull the constraint down because I'm going to use T[K] in a contravariant way! * is basically an alias for any that doesn't get pulled out from constraints, or an inference where we discard the result.
FWIW I don't think it is because type-checking is turned off, but because any is a top type (I don't know if you consider this a distinction). You get the same behavior with unknown.
That is indeed what I meant.
I wonder whether it would be possible to have a special way of marking shapes for types where you really don't care about, or particularly, want their constituent constraints.
I would be more inclined to introduce a new type relationship (e.g. "soundly assignable") that excludes the unsound rule and then use that relationship when checking the extends clause of a conditional type. But we really try hard not to have too many type relationships because we end up consuming more time and memory resources.
Alec Larson (@aleclarson) Yes this seems to be related. The fix made only works for index accesses of the form T[K], where both T and K are parameters; your code uses T['a'].
Using "brackets on a mapped type" inside an "extends" clause lets false extends true evaluate to true.
This isn't 100% true in the sense that what is going on is that the condition false extends T['a'] is being eagerly reduced using the constraint Foo. So the condition is being evaluated as false extends boolean, which correctly selects the true branch. The conditional type is being resolved before the function ever gets called.
Activity
jack-williams commentedon Oct 1, 2018
The issue seems to be the constraint on
T. The following has the intended behavior:The index access expressions
T[K]seems to be pulling in the constraint where all property types extendany, and thereforeundefinedis related.I think this is a minimal repro (using arrays).
The fundamental issue (I think) is that arrays and objects are covariant, which is unsound for update operations.
EDIT: Alternate implementation with constraint:
ahejlsberg commentedon Oct 1, 2018
This one is rather involved. We currently have the following rule:
Sis related to a typeT[K]ifSis related toC, whereCis the base constraint ofT[K].This rule is unsound because
T[K]may be a subtype ofCupon instantiation, but we nonetheless need it for several of reasons. One key reason is the fact that we don't use higher order types for property accesses through type variables. For example, for a property access expressionthis.xxxin a method of a class, we resolve to the declared type ofxxx, not typethis["xxx"], even though the latter is more precise. Effectively, we resolve to the constraint ofthis["xxx"]. We do this for reasons of backwards compatibility and also to avoid drowning in generic types. But it would be odd indeed if the expressionthis.xxxwas not assignable to the typethis["xxx"]--hence the rule that allows the assignment if it matches the constraint.Now, in 3.1 we fixed several issues relating to us not examining all constraints of a type. Because of those fixes we now drill all the way down and find the
anyconstraint forT[K]in your example above. And since anything is assignable toanywe effectively turn off type checking. To wit, none of the following assignments generate errors:That's not good. But the question is how to fix this without breaking the
this.xxxscenario I mentioned above (and other similar ones).So far I think the best contender is to modify the rule above as follows:
Sis related to a typeT[K], whereTandKaren't both type variables, ifSis related toC, whereCis the base constraint ofT[K].In other words, we should limit the unsoundness to situations where one or the other part of the indexed access is an actual zero-order type, but forbid it when both are higher order (since at that point it becomes too unsound).
I ran the experiment on our test suites and there just a few changes, all of which are reasonable. Even better, the RWC suites show no baseline changes at all. I will put this up in a PR.
jack-williams commentedon Oct 2, 2018
FWIW I don't think it is because type-checking is turned off, but because
anyis a top type (I don't know if you consider this a distinction). You get the same behavior withunknown.The conditional type is essentially a type-level version of this:
Am I right in say that this would not address:
type Y<T extends any[]> = undefined extends T[number] ? true : false;?(This isn't a complaint, just wanting to know if I read the rule ok).
From an uninformed/outside perspective it seems like the problem trying to be solved in the initial post is to constrain
Tto some shape (a record, but it could be an array). The programmer only really cares about the outermost data constructor and the any is just there because it causes little friction. Though when you pull down the constraints theanystarts causing problems and you can't control the variance.I wonder whether it would be possible to have a special way of marking shapes for types where you really don't care about, or particularly, want their constituent constraints. Something like:
T extends Record<string, *>can be read as:Tshould be some record but other than that we don't care about the type of the value, please don't pull the constraint down because I'm going to useT[K]in a contravariant way!*is basically an alias foranythat doesn't get pulled out from constraints, or an inference where we discard the result.ahejlsberg commentedon Oct 2, 2018
That is indeed what I meant.
I would be more inclined to introduce a new type relationship (e.g. "soundly assignable") that excludes the unsound rule and then use that relationship when checking the
extendsclause of a conditional type. But we really try hard not to have too many type relationships because we end up consuming more time and memory resources.aleclarson commentedon Nov 4, 2018
Is this related? playground
Using "brackets on a mapped type" inside an "extends" clause lets
false extends trueevaluate to true.jack-williams commentedon Nov 5, 2018
Alec Larson (@aleclarson) Yes this seems to be related. The fix made only works for index accesses of the form
T[K], where bothTandKare parameters; your code usesT['a'].This isn't 100% true in the sense that what is going on is that the condition
false extends T['a']is being eagerly reduced using the constraintFoo. So the condition is being evaluated asfalse extends boolean, which correctly selects the true branch. The conditional type is being resolved before the function ever gets called.