Open ienzam opened 5 years ago
I believe -?
removes undefined
intentionally, because ?
adds it.
Optional
and undefined
are two different things. Optional
adds undefined
which is expected.
But removing Optional
shouldn't remove explicitly typed undefined
(by union).
Unfortunately missing and undefined aren't consistently two different things in TypeScript; see #13195.
Relevant documentation on the behavior of -?
:
Note that in
--strictNullChecks
mode, when a homomorphic mapped type removes a?
modifier from a property in the underlying type it also removesundefined
from the type of that property
But in
{[K in keyof T]-?: 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:
type NullablyRequired<T> = { [P in (keyof T & keyof any)]: T[P] }
type Test = NullablyRequired<{a?: string, b: number}>
// type Test = {a: string | undefined, b: number}
@weswigham I think it should at least work when you explicitly declare undefined
to be a valid value, but it doesn't.
interface OptClass {
opt?: number | undefined;
}
Wow had no idea there's -?
... is there a way to remove | undefined
somehow - so that something like string | undefined
would become simply string
?
Yeah I ran into this as well, i wanted to define this type but it seem like, as @jcalz suggested, the -?
removes the optionality from the resulting type instead of the input type:
// this doesn't work
export type RequireOptionalKeysToBeSpecified<O> = {
[K in keyof O]-?: O[K] extends undefined ? O[K] | undefined : O[K]
}
edit: @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:
edit2: nevermind, you have answered that on SO already πΉ https://stackoverflow.com/a/59791889/2544629
The following works and is the most obvious:
export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
Thanks for the workarounds! But could somebody please explain how this works?
type NullablyRequired<T> = { [P in (keyof T & keyof any)]: T[P] }
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
@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.
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:type NullablyRequired<T> = { [P in (keyof T & keyof any)]: T[P] } type Test = 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
type NullablyRequireExtendableInterface = {
[x: string]: any;
[x: number]: any;
}
However, the other workaround does seem to handle this scenario
export interface ExtendableInterface {
foo?: string | undefined;
[k: string]: any;
}
type NullablyRequired<T> = { [P in keyof Required<T>]: T[P] }
type NullablyRequireExtendableInterface = NullablyRequired<ExtendableInterface>;
// This errors that `foo` needs to be defined
const n: NullablyRequireExtendableInterface = {
}
This is never changing, right? Can we "won't fix" this and just tell people to work around it?
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.
export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
gordonmleigh you absolute king π
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.
The following works and is the most obvious:
export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
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.
The following works and is the most obvious:
export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
Unfortunately, there's an odd behavior with this solution. It looks correct only until you access it.
type Properties = NonPartial<{ options?: object }>; // { options: object }
type Options = Properties['options']; // object | undefined
The following example would have been the expected behavior.
type Properties = Required<{ options?: object }>; // { options: object }
type Options = Properties['options']; // object
The above might just be a display issue as shown here. This in turn likely would mean its a duplicate of https://github.com/microsoft/TypeScript/issues/59948 and that it might get fixed by https://github.com/microsoft/TypeScript/pull/59957
TypeScript Version: 3.3.3333
Search Terms: NonPartial, remove optional modifier
Code
Expected behavior:
NonPartialish.opt
type should supportundefined
.Actual behavior:
NonPartialish.opt
type does not supportundefined
.Playground Link: https://www.typescriptlang.org/play/#src=%2F%2F%20A%20*self-contained*%20demonstration%20of%20the%20problem%20follows...%0D%0A%2F%2F%20Test%20this%20by%20running%20%60tsc%60%20on%20the%20command-line%2C%20rather%20than%20through%20another%20build%20tool%20such%20as%20Gulp%2C%20Webpack%2C%20etc.%0D%0A%0D%0Ainterface%20OptClass%20%7B%0D%0A%20%20opt%3F%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Atype%20NonPartialIsh%20%3D%20%7B%5BK%20in%20keyof%20OptClass%5D-%3F%3A%20OptClass%5BK%5D%20%7C%20undefined%7D%3B%0D%0A%0D%0Aconst%20test%20%3D%20%7Bopt%3A%20undefined%7D%3B%0D%0A%0D%0Averify%3CNonPartialIsh%3E(test)%3B%20%20%2F%2F%20should%20NOT%20be%20error%2C%20but%20shows%20error%0D%0A%0D%0Afunction%20verify%3CT%3E(a%3A%20T)%20%7B%20%7D%0D%0A
Related Issues: