镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Treating undefined parameters as optional #12400

Description

@unional

tsc: 2.2.0-dev.20161116

Discover this behavior from redux-utilities/flux-standard-action@78a9065

This is the original code:

export interface FSA<Payload, Meta> {
  ...
  payload?: Payload;
  ...
}

However, this does not work well with type guard:

function isSomeAction(action: any): action is FSA<{ message: string }, void> {
  return true;
}

let action = {...};
if (isSomeAction(action)) {
  // `action.payload` may be undefined.
  console.log(action.payload.message);
}

Since generic type can be anything, including undefined, I'm considering to remove the optional designation:

export interface FSA<Payload, Meta> {
  ...
  payload: Payload;
  ...
}

// now type guard works
let action = {...};
if (isSomeAction(action)) {
  console.log(action.payload.message);
}

However, now the creation code fail:

function createSomeAction(): FSA<undefined, undefined> {
  // error: `payload` and `meta` are required
  return { type: 'SOME_ACTION' };
}

The workaround is to depends on infer type:

function createSomeAction() {
  return { type: 'SOME_ACTION' };
}

or specify it:

function createSomeAction(): FSA<undefined, undefined> {
  return { 
    type: 'SOME_ACTION',
    payload: undefined,
    meta: undefined
  };
}

Is there a better solution? 🌷

Activity

  1. unional commented on Nov 21, 2016

    @unional
    ContributorAuthor

    I understand that this is mixing two different contexts: Type of the value and type of the reference. But it would be nice to have a better solution to describe this situation?

  2. unional commented on Nov 21, 2016

    @unional
    ContributorAuthor

    As a counter argument, it does make sense that the property should be not optional redux-utilities/flux-standard-action#45 (comment)

  3. unional commented on Jan 26, 2017

    @unional
    ContributorAuthor

    A simpler example:

    function createCounter<T>(x: number) {
      return (t: T) => {
        return x + 1
      }
    }
    
    const count = createCounter<undefined>(1)
    count() // error, have to do `count(undefined)`
  4. unional commented on Feb 3, 2017

    @unional
    ContributorAuthor

    Ryan Cavanaugh (@RyanCavanaugh) Mohamed Hegazy (@mhegazy) do you have any feedback on this? 🌷

    Can this be covered in default generic type when specifying the default type is undefined?

  5. mhegazy commented on Feb 3, 2017

    @mhegazy
    Contributor

    There is nothing specific to generics here. for example:

    declare function f(a: string | undefined): void;
    
    f(); // Not allowed
    f(undefined); // OK
  6. changed the title [-]Accepting `undefined` for generic as optional[/-] [+]Treeting `undefined` parameters as optional[/+] on Feb 3, 2017
  7. changed the title [-]Treeting `undefined` parameters as optional[/-] [+]Treating `undefined` parameters as optional[/+] on Feb 12, 2017
  8. unional commented on Apr 22, 2017

    @unional
    ContributorAuthor

    With generic defaults (#13487) landed, more people will encounter this issue. Should this be fixed?

    i.e.:

    export interface FSA<Payload = never> {
      payload: Payload;
    }
    
    export class SomeFSA implements FSA {
      // error. `payload` is missing
    }
  9. deregtd commented on Nov 8, 2017

    @deregtd

    Adding a little more fuel to the fire. We have our own Promise library (Microsoft/SyncTasks on GitHub) and are running across bugs that devs are introducing now that we've switched our codebase to strict null-compliant.

    In a perfect world, we would like:

    let a: Promise<number>;
    a.resolve(); // not allowed
    a.resolve(4); // ok
    
    let b: Promise<void>;
    b.resolve(); // ok
    b.resolve(4); // not ok
    
    let c: Promise<number|undefined>;
    c.resolve() // not ok
    c.resolve(4) // ok
    c.resolve(undefined) // ok

    But there isn't currently any SFINAE-style way to select valid methods based on T types. If T is void, then you shouldn't be passing anything to resolve, and the function will just get an undefined parameter passed.

    Right now, the function signature is "resolve(param?: T)", but that lets you do:

    SyncTasks.Resolved() -- which then has a resolved synctask with an undefined value, which will then explode anything downstream that doesn't allow an undefined value.

    We're contemplating, for now, changing synctasks to require always taking a parameter, and just adding some void helpers to make the code less annoyingly verbose, but it's ugly compared to what we could do with method signature selectors.

  10. deregtd commented on Nov 9, 2017

    @deregtd

    To be clear, I don't actually want fully SFINAE-able method selectors -- this is JS after all, you can't do that. But I think the real answer is that if you have a function parameter whose type is void, then it should auto-change the signature for that parameter to optional, and error if you pass anything other than undefined to it, if you DO pass a value.

  11. 25 remaining items

  12. DScheglov commented on May 14, 2021

    @DScheglov

    Hey, Seph Reed (@SephReed)

    your workaround is a correct approach. But with minor update:

    class Base<A extends any[]> {
      public foo(...args: A){}
      public bar(...args: A){}
      public qux(...args: A){}
    }
  13. ericwooley commented on May 16, 2021

    @ericwooley

    There are lots of workarounds here, Maybe all that's really needed is some good documentation or utility types?

  14. jacob-haller-roby commented on Jul 1, 2021

    @jacob-haller-roby

    Trivial change, but my preferred method after playing around with the suggestions in this thread:

    class Base<T = any> {
      public foo(...arg: T[]){}
      public bar(...arg: T[]){}
      public qux(...arg: T[]){}
    }
    
    class AllRequired extends Base<string> { }
    class AllOptional extends Base { }
    
    const req = new AllRequired();
    req.foo(); // without an arg, this will throw an error
    
    const opt = new AllOptional();
    req.opt(); // this is fine though
  15. freakzlike commented on Aug 2, 2021

    @freakzlike

    I tried to use void with conditional types, but i still need to pass undefined as parameter.

    interface State {
      counter: number
    }
    
    interface Mutations {
      reset: (state: State) => void
      add: (state: State, value: number) => void
    }
    
    interface ActionContext {
      commit<K extends keyof Mutations>(
        key: K,
        payload: Parameters<Mutations[K]>[1] extends undefined ? void : Parameters<Mutations[K]>[1]
      ): void
    }
    
    const action = ({ commit }: ActionContext) => {
      commit('reset') // Expected 2 arguments, but got 1.
      commit('reset', undefined) // works
      commit('add', 1) // works
    }
  16. DScheglov commented on Aug 2, 2021

    @DScheglov

    freakzlike ,
    for parameters it works in a little bit another way:

    interface State {
      counter: number
    }
    
    interface Mutations {
      reset: (state: State) => void
      add: (state: State, value: number) => void
    }
    
    type Payload<M> = M extends (state: any, ...payload: infer P) => void ? P : never;
    
    type ActionContext = {
        commit: <Key extends keyof Mutations>(key: Key, ...payload: Payload<Mutations[Key]>) => void;
    }
    
    const action = ({ commit }: ActionContext) => {
      commit('reset') // works
      commit('reset', undefined) // Expected 1 arguments, but got 2.
      commit('add', 1) // works
    }

    See in Playground

    Also I suggest to make ActionContext generic because it is the same for any possible Mutations:

    type ActionContext<Ms extends {}> = {
        commit: <Key extends keyof Ms>(key: Key, ...payload: Payload<Ms[Key]>) => void;
    }
    
    const action = ({ commit }: ActionContext<Mutations>) => {
      commit('reset') // works
      commit('reset', undefined) // Expected 1 arguments, but got 2.
      commit('add', 1) // works
    }
  17. Paduado commented on Nov 27, 2021

    @Paduado

    A simpler example:

    function createCounter<T>(x: number) {
      return (t: T) => {
        return x + 1
      }
    }
    
    const count = createCounter<undefined>(1)
    count() // error, have to do `count(undefined)`

    Solved it like this:

    function createCounter<T>(x: number) {
      return (...args: undefined extends T ? [] : [T]) => {
        return x + 1;
      };
    }
    
    const count = createCounter<undefined>(1);
    count();
  18. data-enabler commented on Oct 11, 2022

    @data-enabler

    I'd also like to add that being able to tell Typescript to treat properties that can be undefined as optional would be useful for interop with Closure Compiler annotations, since their type system makes no distinction between the two and doesn't support any optional property syntax.

  19. unional commented on Oct 12, 2022

    @unional
    ContributorAuthor

    After 6 years since I raise this issue, I realized that what I really want is the other way around. Instead of "treating undefined as optional", I want it to be more expressive to say when the type is optional.

    i.e. something like:

    type FSA<Payload, Meta> = Equal<Payload, void> 
      ? { ... }
      : { payload: Payload }

    There is a semantic difference between undefined and optional.
    I don't want this suggestion makes them even more muddy.

  20. data-enabler commented on Oct 12, 2022

    @data-enabler

    There is a semantic difference between undefined and optional. I don't want this suggestion makes them even more muddy.

    Unfortunately, every other issue asking for the behavior I'm describing has been marked as a duplicate of this one. Perhaps it would make sense to open a separate issue?

  21. unional commented on Oct 12, 2022

    @unional
    ContributorAuthor

    Unfortunately, every other issue asking for the behavior I'm describing has been marked as a duplicate of this one. Perhaps it would make sense to open a separate issue?

    It is still the same issue. What I'm suggesting is that the solution should not "loosen" the TypeScript semantics. There should be an alternative to describe precisely what it is.

    There are workarounds for some cases such as the one suggested here: #12400 (comment)

    But it would be great if TS can provide a way to describe it out of the box without causing more confusion.

    Additional food of thoughts: what is void and what is never? Does void maps to unit in Haskell and never maps to Void in Haskell?

    Is there a way to use them in generics to precisely describe what we want here?

    For those who are interested in type theory, I would recommend checking out Category Theory for Programmers by Bartosz Milewski

  22. chharvey commented on Jul 7, 2023

    @chharvey

    I disagree with treating undefined parameters as optional. If a function explicitly defines a parameter as potentially undefined, it should expect that argument to be provided.

    function f(n: number | undefined): boolean { return !!n; }
    f(); // is an error, as it should be (incorrect number of arguments)

    However, I would like to propose the opposite: treating optional parameters as non-undefined (unless explicitly specified). This would be aligned with the exactOptionalPropertyTypes compiler option. Perhaps add a new exactOptionalParameterTypes option?

    function f(n?: number): boolean { return !!n; }
    f();          // allowed
    f(undefined); // should be an error! (but is not currently)
    
    function g(n?: number | undefined): boolean { return !!n; }
    g();          // allowed
    g(undefined); // allowed, but only because it satisfies the expected type

    This aligns with object properties:

    type F = {n?: number};
    const f1: F = {}; // allowed
    const f2: F = {n: undefined}; // is an error (under exactOptionalPropertyTypes), as it should be
    
    type G = {n?: number | undefined};
    const g1: G = {};             // allowed
    const g2: G = {n: undefined}; // allowed
  23. christianalfoni commented on Jan 9, 2024

    @christianalfoni

    Hi there 😄

    So I just wanted to add a simple example with a few iterations I have been reflecting on.

    We are creating a signal. A signal is simply a function that returns an object with a value property. The typing reflects the value of the signal, though you can pass in an initialValue as a param to the function.

    Example 1

    function signal<T>(initialValue: T) {
      return { value: initialValue }
    }
    
    // OK
    const count = signal<number | undefined>(0)
    
    // OK
    const count = signal<number | undefined>(undefined)
    
    // NOT OK
    const count = signal<number | undefined>()
    
    // NOT OK, and void does not reflect the actual value of the signal
    const count = signal<number | void>()

    Example 2

    function signal<T>(): { value: T | undefined }
    function signal<T>(initialValue: T): { value: T }
    function signal<T>(initialValue?: T) {
      return { value: initialValue }
    }
    
    // OK
    const count = signal<number>(0)
    
    // OK, though implicit through the function override that the signal
    // type becomes `number | undefined`
    const count = signal<number>()

    Example 3

    function signal<T>(
      ...[initialValue]: (T extends undefined ? [] | [initialValue: T] : [initialValue: T])
    ) {
      return { value: initialValue }
    }
    
    // OK
    const count = signal<number | undefined>(0)
    
    // OK, though the param signature/errors of the initialValue is confusing to read
    const count = signal<number | undefined>()

    So just wanted to share this to add to the discussion.

    For me, as the typing reflects the actual value of the signal, I have chosen to go with Example 1 in this case. It arguably gives the most predictable result and developer experience.

  24. phaux commented on Sep 11, 2025

    @phaux

    workaround:

    /** Create a tuple with element which is optional if it is nullable. */
    export type optArg<T> = undefined extends T ? [T?] : [T]

    usage:

    function test<T>(normalArg: string, ...[maybeOptionalArg]: optArg<T>) {}
  25. zeorin commented on Oct 3, 2025

    @zeorin

    Niki (@phaux) I adapted your approach to an arbitrary number of arguments:

    type OptionalArgs<T extends [...unknown[]]> =
     & { [I in keyof T as (undefined extends T[I] ? I : never) & `${number & {}}`]?: T[I]; }
     & { [I in keyof T as (undefined extends T[I] ? never : I) & `${number & {}}`]:  T[I]; }
     & ReadonlyArray<unknown>;
    
    const one = (...[first]: OptionalArgs<[first: number | undefined]>) => {
      first;
      // ^?: number | undefined
    };
    const two = (...[first, second]: OptionalArgs<[first: number | undefined, second: string | undefined]>) => {
      first;
      // ^?: number | undefined
      second;
      // ^?: string | undefined
    };
    
    one();
    one(undefined);
    one(42);
    // @ts-expect-error
    one('foo');
    
    two();
    two(undefined);
    two(undefined, undefined);
    two(42);
    two(undefined, 'forty two');
    // @ts-expect-error
    two('foo');
    // @ts-expect-error
    two('foo', undefined);
    // @ts-expect-error
    two('foo', 42);
    // @ts-expect-error
    two(undefined, 42);
    // @ts-expect-error
    two('foo', 'bar');

    Playground

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

    CommittedThe team has roadmapped this issueHelp WantedYou can do thisSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions