microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.71k stars 12.45k forks source link

Removing optional modifier also removes `undefined` from value type #31025

Open ienzam opened 5 years ago

ienzam commented 5 years ago

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:

Actual behavior:

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.

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 commented 5 years ago

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

ienzam commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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;
}
jayarjo commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

The following works and is the most obvious:

export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
Torvin commented 3 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 1 year ago

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

Andarist commented 1 year ago

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 commented 1 year ago
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.

llamahunter commented 3 months ago

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 commented 2 weeks ago

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 commented 2 weeks ago

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