Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removing optional modifier also removes undefined from value type #31025

Open
ienzam opened this issue Apr 18, 2019 · 18 comments
Open

Removing optional modifier also removes undefined from value type #31025

ienzam opened this issue Apr 18, 2019 · 18 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@ienzam
Copy link

ienzam commented Apr 18, 2019

TypeScript Version: 3.3.3333

Search Terms: NonPartial, remove optional modifier

Code

// 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.

interface OptClass {
  opt?: number;
}

type NonPartialIsh = {[K in keyof OptClass]-?: OptClass[K] | undefined};

const test = {opt: undefined};

verify<NonPartialIsh>(test);  // should NOT be error, but shows error

function verify<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.

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:

@weswigham
Copy link
Member

I believe -? removes undefined intentionally, because ? adds it.

@ienzam
Copy link
Author

ienzam commented Apr 18, 2019

Optional and undefined are two different things. Optional adds undefined which is expected.
But removing Optional shouldn't remove explicitly typed undefined (by union).

@jcalz
Copy link
Contributor

jcalz commented Apr 19, 2019

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 removes undefined 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}

@MartinJohns
Copy link
Contributor

@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;
}

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript Bug A bug in TypeScript and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Apr 22, 2019
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Apr 22, 2019
@jayarjo
Copy link

jayarjo commented Jul 23, 2020

Wow had no idea there's -?... is there a way to remove | undefined somehow - so that something like string | undefined would become simply string?

@ekilah
Copy link

ekilah commented Jan 13, 2021

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

@gordonmleigh
Copy link

The following works and is the most obvious:

export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };

@Torvin
Copy link

Torvin commented Sep 28, 2021

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

@ekilah
Copy link

ekilah commented 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
Copy link

Woodz commented Jan 7, 2022

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 = {

}

@jcalz
Copy link
Contributor

jcalz commented May 8, 2023

This is never changing, right? Can we "won't fix" this and just tell people to work around it?

@Andarist
Copy link
Contributor

Andarist commented 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 | undefined that is added by the template is super surprising.

@2bam
Copy link

2bam commented May 8, 2023

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.

Offroaders123 added a commit to Offroaders123/NBTify that referenced this issue Jan 19, 2024
Now the format of the output file is combined into a full object, which is then used by the main file, rather than importing all of the arguments as individual flags.

Added a flag to specify the SNBT indentation spacing! It was only set to 2 before, now you can have fully minified SNBT, or use any indentation you'd like. Like the programming NBTify API, it accepts a number or a string.

Removed the `--pipe` flag, in favor of simply specifying whether you want to use an NBT or SNBT output to stdout. If neither `--nbt` or `--snbt` are passed as flags, it will simply log out the structure of the file, with nice pretty-printing, colors, and such, as it currently has been doing thus far.

The pretty-print logging now fully logs out the NBT file's tree, other than for TypedArray-based tags. For everything else though, you see all of it's content, rather than being abbreviated (the normal behavior for `console.log()` in Node.js).

Learned about the difference between `isNan()` and `Number.isNaN()`. The first one coerces the input value, checks if it's `NaN`, while the other simply checks what it is currently (doesn't convert to `number` first).

#25

microsoft/TypeScript#31025
https://stackoverflow.com/questions/56143158/how-to-use-util-promisify-and-bind-functions-in-nodejs (I think I ran into this issue previously, but I didn't know that function binding would work nicely here!)
https://stackoverflow.com/questions/43362222/nodejs-short-alias-for-process-stdout-write
https://askubuntu.com/questions/510890/how-do-i-redirect-command-output-to-vim-in-bash (Looked into this again, was curious in whether it's viable to edit SNBT with Vim again)

Oh yeah, that reminds me, I also made it so SNBT is added to stdout with a trailing `\n`, so it nicer looking in the terminal, as well as in the editor, as most editors utilize trailing new lines. I think those are finally catching on with me! They really bugged me for some reason, since the start of my programming lol. Now I'm getting Linux'ed haha.

nodejs/node#20366
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
https://www.google.com/search?q=comments+in+powershell
@llamahunter
Copy link

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.

@DrafaKiller
Copy link

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

@Andarist
Copy link
Contributor

Andarist commented 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

@juhort
Copy link

juhort commented Dec 9, 2024

@jcalz This seems to happen because of homomorphism and not because of -?.

If we add -? to your snippet mentioned here.

type NullablyRequired<T> = { [P in (keyof T & keyof any)]-?: T[P] } // Note the addition of `-?`

type Test = NullablyRequired<{a?: string, b: number}>
// type Test = {a: string | undefined, b: number}

The type of Test still remains the same. If -? were suppose to swallow the | undefined, then it should have been swallowed in this case as well and the type of Test should have been {a: string, b: number}, just like in the following case (where the mapped type is homomorphic).

type NullablyRequired<T> = { [P in keyof T]-?: T[P] }

type Test = NullablyRequired<{a?: string, b: number}>
// type Test = {a: string, b: number}

The only difference between the two snippets above is that the latter is homomorphic while the former is not.

@jcalz
Copy link
Contributor

jcalz commented 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests