undefined extends all types when accessing a generic in a mapped typed #27470
Copy link
Copy link
Closed
Labels
BugA bug in TypeScriptA bug in TypeScriptFixedA PR has been merged for this issueA PR has been merged for this issue
Milestone
Description
TypeScript Version: 3.2.0-dev.20180929 (First noticed in 3.1.1)
strictNullChecks must be on otherwise this is expected behaviour.
It seems that an issue has been introduced when accessing attributes of a mapped type in a conditional type.
Search Terms:
conditional type undefined extends
mapped type undefined extends
conditional type in mapped type access
Code
type UndefinedKeys<T extends Record<string, any>> = {
[K in keyof T]: undefined extends T[K] ? K : never
};
type MyType = {a: string, b: string | undefined}
type Result1 = UndefinedKeys<MyType>;
const a1: Result1['a'] = 'a'; // $ExpectError
~~ There should be an error here
const b1: Result1['b'] = 'b';
// manually mapping the type, error is show as expected
type Result2 = {
a: undefined extends MyType['a'] ? 'a' : never;
b: undefined extends MyType['b'] ? 'b' : never;
};
const a2: Result2['a'] = 'a'; // $ExpectError
const b2: Result2['b'] = 'b';To simplify this even more line 5 can be changed to
type MyType = { a: string, b: undefined };Expected behavior:
- The type
Result1should be{a: never; b: 'b'}. - Line 9 should have an error:
Type '"a"' is not assignable to type 'never'.
Actual behavior:
- The type
Result1is{a: 'a'; b: 'b'}. - Line 9 has no error.
Playground Link:
| TypeScript Version | Playground Link |
|---|---|
| 2.8.1 | Playground |
| 3.0.1 | Playground |
| 3.1.6 | Playground |
| 3.2.1 | Playground |
| 3.2.1-insiders.20181128 (Official Playground) | Playground <---- ENABLE strictNullChecks IN OPTIONS |
| 3.3.0-dev.20190104 | Fixed? |
Related Issues:
Potentially: #26942
Initially thought it was an issue with Distributive conditional types but the Generic type is in the assignable to position.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScriptFixedA PR has been merged for this issueA PR has been merged for this issue
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.