Repository navigation
undefined extends all types when accessing a generic in a mapped typed #27470
Description
Activity
jack-williams commented
on Oct 1, 2018 CollaboratorMore actionsThe issue seems to be the constraint on
T. The following has the intended behavior:type UndefinedKeys<T> = { [K in keyof T]: undefined extends T[K] ? K : never };
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).
type X<T extends any> = undefined extends T ? true : false; type Y<T extends any[]> = undefined extends T[number] ? true : false; type A = X<number>; // ignores the `any` constraint and returns false type B = Y<number[]>; // returns true;
The fundamental issue (I think) is that arrays and objects are covariant, which is unsound for update operations.
EDIT: Alternate implementation with constraint:
type UndefinedKeys<T extends Record<string, any>> = { [K in keyof T]: undefined extends Extract<T[K], undefined> ? K : never; };
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Oct 1, 2018 This one is rather involved. We currently have the following rule:
- A type
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:function test<T extends Record<string, any>, K extends keyof T>(t: T, k: K) { t[k] = 42; t[k] = "hello"; t[k] = undefined; }
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:
- A type
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.
- A type
- addedBugA bug in TypeScriptA bug in TypeScriptFixedA PR has been merged for this issueA PR has been merged for this issueand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Oct 1, 2018 jack-williams commented
on Oct 2, 2018 CollaboratorMore actionsAnd 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
anyis a top type (I don't know if you consider this a distinction). You get the same behavior withunknown.type UndefinedKeys<T extends Record<string, unknown>> = { [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'; // no error
The conditional type is essentially a type-level version of this:
const x: {a: string} = { a: "a" }; const baseConstraint: Record<string, unknown> = x; baseConstraint.a = undefined; // unsound
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].
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).
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
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:
type UndefinedKeys<T extends Record<string, *>> = { [K in keyof T]: undefined extends T[K] ? K : never };
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.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
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.Is this related? playground
Using "brackets on a mapped type" inside an "extends" clause lets
false extends trueevaluate to true.jack-williams commented
on Nov 5, 2018 CollaboratorMore actionsAlec 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'].Using "brackets on a mapped type" inside an "extends" clause lets
false extends trueevaluate 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 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.Reacted by Alec Larson- locked as resolved and limited conversation to collaborators
on Oct 21, 2025
TypeScript Version:
3.2.0-dev.20180929(First noticed in3.1.1)strictNullChecksmust 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
To simplify this even more line 5 can be changed to
Expected behavior:
Result1should be{a: never; b: 'b'}.Type '"a"' is not assignable to type 'never'.Actual behavior:
Result1is{a: 'a'; b: 'b'}.Playground Link:
strictNullChecksIN OPTIONSRelated Issues:
Potentially: #26942
Initially thought it was an issue with Distributive conditional types but the Generic type is in the assignable to position.