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
Removing optional modifier also removes undefined from value type #31025
// A *self-contained* demonstration of the problem follows...// Test this by running `tsc` on the command-line, rather than through another build tool such as Gulp, Webpack, etc.interfaceOptClass{opt?: number;}typeNonPartialIsh={[KinkeyofOptClass]-?: OptClass[K]|undefined};consttest={opt: undefined};verify<NonPartialIsh>(test);// should NOT be error, but shows errorfunctionverify<T>(a: T){}
Expected behavior:
Should not have any error.
NonPartialish.opt type should support undefined.
Actual behavior:
Throws the following error:
ERROR(12,23): : Argument of type '{ opt: undefined; }' is not assignable to parameter of type 'NonPartialIsh'.
Types of property 'opt' are incompatible.
Type 'undefined' is not assignable to type 'number'.
Transpiled code follows despite errors.
NonPartialish.opt type does not support undefined.
Optional and undefined are two different things. Optional adds undefined which is expected.
But removing Optional shouldn't remove explicitly typed undefined (by union).
Reacted by Richard Wise, Martin 2bam, public-hosting, Richard Lee, Antoine Hébert, Maxwell DeVos, Artiom Neganov, George Thomas Madeley and Matthew Costello
Note that in --strictNullChecks mode, when a homomorphic mapped type removes a ? modifier from a property in the underlying type it also removes undefined from the type of that property
But in
{[KinkeyofT]-?: Foo<T[K]>}
should undefined be excluded from the original type of the property (T[K]) or the mapped type of the property (Foo<T[K]>)? I'd kind of expect it to be the former but it looks like it's actually the latter.
Assuming we can't destabilize the current behavior, someone who wants to strip the optional modifier off property keys but hold on to undefined in their values could do so by preventing the compiler from recognizing the mapped type as homomorphic:
typeNullablyRequired<T>={[Pin(keyofT&keyofany)]: T[P]}typeTest=NullablyRequired<{a?: string,b: number}>// type Test = {a: string | undefined, b: number}
Reacted by S. B. Tam, Md. Enzam Hossain, Matthias, Monroe Ekilah, 流浪大法师, Vladimir, Markus Nüssler, Can Tecim, Antoine Hébert, Sterling Camden and 4 more
Yeah I ran into this as well, i wanted to define this type but it seem like, as Joe Calzaretta (@jcalz) suggested, the -? removes the optionality from the resulting type instead of the input type:
// this doesn't workexporttypeRequireOptionalKeysToBeSpecified<O>={[KinkeyofO]-?: O[K]extendsundefined ? O[K]|undefined : O[K]}
edit: Joe Calzaretta (@jcalz) do you mind explaining what you meant here? your solution works but I don't quite follow you, would love to understand it better:
preventing the compiler from recognizing the mapped type as homomorphic:
Reacted by Regev Brody, B. Bergeron and Maxwell DeVos
Reacted by Tim Kinnane, Vladimir, Torvin, Giovanni Gonzaga, Marcos Mariani, Ariel Barabas, Fabian Hiller, Martin 2bam, Dieter Geerts, Simon Tran and 12 more
What's the significance of adding keyof any here? I'm guessing it has to do with "preventing the compiler from recognizing the mapped type as homomorphic" but would be nice if somebody could provide an explanation
Basically & keyof any "breaks" TS's assumption that the input and output type should stay "strongly linked" to each other, if you'll forgive my oversimplification.
Assuming we can't destabilize the current behavior, someone who wants to strip the optional modifier off property keys but hold on to undefined in their values could do so by preventing the compiler from recognizing the mapped type as homomorphic:
typeNullablyRequired<T>={[Pin(keyofT&keyofany)]: T[P]}typeTest=NullablyRequired<{a?: string,b: number}>// type Test = {a: string | undefined, b: number}
Unfortunately this doesn't seem to work for interfaces that include dynamic fields, e.g.
export interface ExtendableInterface {
foo?: string | undefined;
[k: string]: any;
}
type NullablyRequired<T> = { [P in (keyof T & keyof any)]: T[P] }
type NullablyRequireExtendableInterface = NullablyRequired<ExtendableInterface>;
// This should throw compiler error because foo is missing, but it doesn't
const n: NullablyRequireExtendableInterface = {
}
It seems to give up on the non-dynamic fields defined in the interface and just infer
Even if changing some of the mentioned behaviors might be hard to change now (although, personally I think it's better to fix them), I feel like especially the OP's case is quite bizarre and unintuitive for users.
Syntactically -? clearly refers to the field's optionality and to nothing else. Removing explicit | undefined that is added by the template is super surprising.
Reacted by Martin 2bam, Dieter Geerts, brettbedarfmw, Jason Killian, mtone, Artiom Neganov, Vladyslav Nazarenko and Matthew Costello
Probably hard to change the behavior of -? without breaking everything. But when developers are opting-in explicitly specifying | undefined ourselves, the majority I think will expect it to respect that. It's confusing when it doesn't and key-optionality interferes with field-type. My use case is to syntax check for type completion including optionals when patching objects, in case the type changes.
Note that this doesn't seem to actually work unless you are using typescript 5.5 or later. Prior to that, it just copies through the optionality of all fields.
I thought it was apparent that the issue has to do with using -?on a homomorphic mapped type, as using -? on a non-homomorphic mapped type is a no-op (those properties wouldn't be optional anyway). See #21919 for how this was implemented (and note that it explicitly mentions operating on homomorphic mapped types).
Activity
weswigham commentedon Apr 18, 2019
I believe
-?removesundefinedintentionally, because?adds it.ienzam commentedon Apr 18, 2019
Optionalandundefinedare two different things.Optionaladdsundefinedwhich is expected.But removing
Optionalshouldn't remove explicitly typedundefined(by union).jcalz commentedon Apr 19, 2019
Unfortunately missing and undefined aren't consistently two different things in TypeScript; see #13195.
Relevant documentation on the behavior of
-?:But in
should
undefinedbe excluded from the original type of the property (T[K]) or the mapped type of the property (Foo<T[K]>)? I'd kind of expect it to be the former but it looks like it's actually the latter.Assuming we can't destabilize the current behavior, someone who wants to strip the optional modifier off property keys but hold on to
undefinedin their values could do so by preventing the compiler from recognizing the mapped type as homomorphic:MartinJohns commentedon Apr 19, 2019
Wesley Wigham (@weswigham) I think it should at least work when you explicitly declare
undefinedto be a valid value, but it doesn't.jayarjo commentedon Jul 23, 2020
Wow had no idea there's
-?... is there a way to remove| undefinedsomehow - so that something likestring | undefinedwould become simplystring?ekilah commentedon Jan 13, 2021
Yeah I ran into this as well, i wanted to define this type but it seem like, as Joe Calzaretta (@jcalz) suggested, the
-?removes the optionality from the resulting type instead of the input type:edit:
Joe Calzaretta (@jcalz) do you mind explaining what you meant here? your solution works but I don't quite follow you, would love to understand it better:
edit2: nevermind, you have answered that on SO already 😹 https://stackoverflow.com/a/59791889/2544629
gordonmleigh commentedon Jun 28, 2021
The following works and is the most obvious:
Torvin commentedon Sep 28, 2021
Thanks for the workarounds! But could somebody please explain how this works?
What's the significance of adding
keyof anyhere? I'm guessing it has to do with "preventing the compiler from recognizing the mapped type as homomorphic" but would be nice if somebody could provide an explanationekilah commentedon Nov 20, 2021
Torvin I found this answer on SO helpful: https://stackoverflow.com/a/59791889/2544629
Basically
& keyof any"breaks" TS's assumption that the input and output type should stay "strongly linked" to each other, if you'll forgive my oversimplification.Woodz commentedon Jan 7, 2022
Unfortunately this doesn't seem to work for interfaces that include dynamic fields, e.g.
It seems to give up on the non-dynamic fields defined in the interface and just infer
However, the other workaround does seem to handle this scenario
jcalz commentedon May 8, 2023
This is never changing, right? Can we "won't fix" this and just tell people to work around it?
Andarist commentedon May 8, 2023
Even if changing some of the mentioned behaviors might be hard to change now (although, personally I think it's better to fix them), I feel like especially the OP's case is quite bizarre and unintuitive for users.
Syntactically
-?clearly refers to the field's optionality and to nothing else. Removing explicit| undefinedthat is added by the template is super surprising.2bam commentedon May 8, 2023
gordonmleigh you absolute king 👑
Probably hard to change the behavior of
-?without breaking everything. But when developers are opting-in explicitly specifying| undefinedourselves, the majority I think will expect it to respect that. It's confusing when it doesn't and key-optionality interferes with field-type. My use case is to syntax check for type completion including optionals when patching objects, in case the type changes.Logging Depth + Space Flag + Pipe Flag Removal
llamahunter commentedon Jul 4, 2024
Note that this doesn't seem to actually work unless you are using typescript 5.5 or later. Prior to that, it just copies through the optionality of all fields.
DrafaKiller commentedon Oct 2, 2024
Unfortunately, there's an odd behavior with this solution. It looks correct only until you access it.
The following example would have been the expected behavior.
Andarist commentedon Oct 2, 2024
The above might just be a display issue as shown here. This in turn likely would mean its a duplicate of #59948 and that it might get fixed by #59957
jcalz commentedon Dec 9, 2024
I thought it was apparent that the issue has to do with using
-?on a homomorphic mapped type, as using-?on a non-homomorphic mapped type is a no-op (those properties wouldn't be optional anyway). See #21919 for how this was implemented (and note that it explicitly mentions operating on homomorphic mapped types).