Skip to content

Removing optional modifier also removes undefined from value type #31025

Open

Description

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:

Activity

  1. weswigham commented on Apr 18, 2019

    @weswigham
    Member

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

  2. ienzam commented on Apr 18, 2019

    @ienzam
    Author

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

  3. jcalz commented on Apr 19, 2019

    @jcalz
    Contributor

    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}
  4. MartinJohns commented on Apr 19, 2019

    @MartinJohns
    Contributor

    Wesley Wigham (@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;
    }
    
  5. added
    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this feature
    SuggestionAn idea for TypeScript
    BugA bug in TypeScript
    and removed
    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this feature
    SuggestionAn idea for TypeScript
    on Apr 22, 2019
  6. jayarjo commented on Jul 23, 2020

    @jayarjo

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

  7. ekilah commented on Jan 13, 2021

    @ekilah

    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 work
    export type RequireOptionalKeysToBeSpecified<O> = {
      [K in keyof O]-?: O[K] extends undefined ? 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:

    edit2: nevermind, you have answered that on SO already 😹 https://stackoverflow.com/a/59791889/2544629

  8. gordonmleigh commented on Jun 28, 2021

    @gordonmleigh

    The following works and is the most obvious:

    export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
  9. Torvin commented on Sep 28, 2021

    @Torvin

    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

  10. ekilah commented on Nov 20, 2021

    @ekilah

    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.

  11. Woodz commented on Jan 7, 2022

    @Woodz

    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 = {
    
    }
    
  12. jcalz commented on May 8, 2023

    @jcalz
    Contributor

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

  13. Andarist commented on May 8, 2023

    @Andarist
    Contributor

    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.

  14. 2bam commented on May 8, 2023

    @2bam
    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.

  15. llamahunter commented on Jul 4, 2024

    @llamahunter

    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.

  16. DrafaKiller commented on Oct 2, 2024

    @DrafaKiller

    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
  17. Andarist commented on Oct 2, 2024

    @Andarist
    Contributor

    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

  18. jcalz commented on Dec 9, 2024

    @jcalz
    Contributor

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDomain: Mapped TypesThe issue relates to mapped types

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions