TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Treating `undefined` parameters as optional

Open unional opened this issue 8 years ago • 40 comments

tsc: 2.2.0-dev.20161116

Discover this behavior from https://github.com/acdlite/flux-standard-action/pull/45/commits/78a9065914b2ca4848dfba8fc0b47c54e2d0e319

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? 🌷

unional avatar Nov 21 '16 06:11 unional

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?

unional avatar Nov 21 '16 06:11 unional

As a counter argument, it does make sense that the property should be not optional https://github.com/acdlite/flux-standard-action/pull/45#issuecomment-261863311

unional avatar Nov 21 '16 07:11 unional

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)`

unional avatar Jan 26 '17 01:01 unional

@RyanCavanaugh @mhegazy do you have any feedback on this? 🌷

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

unional avatar Feb 03 '17 18:02 unional

There is nothing specific to generics here. for example:

declare function f(a: string | undefined): void;

f(); // Not allowed
f(undefined); // OK

mhegazy avatar Feb 03 '17 22:02 mhegazy

With generic defaults (https://github.com/Microsoft/TypeScript/pull/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
}

unional avatar Apr 22 '17 09:04 unional

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.

deregtd avatar Nov 08 '17 23:11 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.

deregtd avatar Nov 09 '17 00:11 deregtd

Approved. This will probably be a hairy change so we'll take a first stab at it unless someone wants to jump in. We don't think there will be breaking changes but we'll need to investigate

RyanCavanaugh avatar Nov 28 '17 01:11 RyanCavanaugh

RxJS has the exact same problem as described by @deregtd https://github.com/ReactiveX/rxjs/issues/2852 https://github.com/ReactiveX/rxjs/pull/3074

felixfbecker avatar Jan 23 '18 05:01 felixfbecker

any chance for 3.0?

agalazis avatar Jul 29 '18 15:07 agalazis

@agalazis no, it didn’t make it in for 3.0. In any case, Typescript doesn’t accurately model the distinction between missing and undefined right now, so there’s no guarantee fixing just this special case of it will work.

A complete solution would require the introduction of a new subtype of undefined, the missing type. And last time we discussed that at a design meeting, nobody thought it was worth the complexity that would introduce.

sandersn avatar Jul 30 '18 15:07 sandersn

For anyone still waiting for optional function arguments, it is now possible to simulate that using new tuple types and spread expressions:

type OptionalSpread<T = undefined> =
    T extends undefined
    ? []
    : [T]

const foo = <T = undefined>(...args: OptionalSpread<T>): void => {
    const arg = args[0] // Type of: T = undefined
}

// undefined inferred
foo()               // OK
foo(42)             // OK <----single argument type is inferred, can't do anything about it   

// undefined explicit
foo<undefined>()    // OK
foo<undefined>(42)  // ERROR Expected 0 arguments, but got 1.

// number
foo<number>(42)     // OK
foo<number>()       // ERROR Expected 1 arguments, but got 0.
foo<number>("bar")  // ERROR Argument is not assignable to parameter of type 'number'.

it has a limitation with inferred argument type though, which is solved by explicitly specifying undefined argument type

minajevs avatar Oct 10 '18 14:10 minajevs

In my case I wanted to make object properties optional. I had an API spec from which I automatically generated query and URL parameters (using conditional types), and sometimes one or both would be undefined (not required).

Just sharing how I was able to make undefined object parameters optional in case somebody else runs into the same issue:

export type NonUndefinedPropertyNames<T> = {
  [K in keyof T]: T[K] extends undefined ? never : K
}[keyof T]

export type OnlyRequired<T> = Pick<T, NonUndefinedPropertyNames<T>>

Example:

type Args1 = OnlyRequired<{      // {query: {a: number, b: string}, {c: number}}
  query: {a: number, b: string}
  params: {c: number}
}>

type Args2 = OnlyRequired<{      // {query: {a: number, b: string}}
  query: {a: number, b: string}
  params: undefined
}>

type Args3 = OnlyRequired<{      // {}
  query: undefined
  params: undefined
}>

const a: Args1 = {
  query: {a: 1, b: 'two'},
  params: {c: 3},
}

const b: Args2 = {
  query: {a: 1, b: 'two'},
}

const c: Args3 = {}

jeremija avatar Apr 02 '19 06:04 jeremija

For anyone wanting to make all properties that are undefined-able optional, here you go. The magic sauce is the last type, UndefinedOptional<T>.

Updated Gist: https://gist.github.com/travigd/18ae344a6bc69074b17da11333835c3d#file-undefined-optional-ts

/**
 * Get all of the keys to which U can be assigned.
 */
type OnlyKeys<T, U> = {
  [K in keyof T]: U extends T[K] ? K : never
}[keyof T];

/**
 * Get the interface containing only properties to which U can be assigned.
 */
type OnlyUndefined<T> = {
  [K in OnlyKeys<T, undefined>]: T[K]
}

/**
 * Get all of the keys except those to which U can be assigned.
 */
type ExcludeKeys<T, U> = {
  [K in keyof T]: U extends T[K] ? never : K
}[keyof T];

/**
 * Get the interface containing no properties to which U can be assigned.
 */
type ExcludeUndefined<T> = {
  [K in ExcludeKeys<T, undefined>]: T[K]
}

/**
 * Get the interface where all properties are optional.
 */
type Optional<T> = {[K in keyof T]?: T[K]};

/**
 * Get the interface where properties that can be assigned undefined are
 * also optional.
 */
type UndefinedOptional<T> = ExcludeUndefined<T> & Optional<OnlyUndefined<T>>;

Example

interface Address {
  lineOne: string;
  lineTwo: string | undefined;
  zip: number;
}
type OptionalAddress = UndefinedOptional<Address>;
const addr: OptionalAddress = {
  lineOne: "1234 Main St.",
  lineTwo: "Suite 123",
  zip: 55555,
};

const addr2: OptionalAddress = {
  lineOne: "1234 Main St.",
  zip: 55555,
};

const addr3: OptionalAddress = {
  lineOne: "1234 Main St.",
  lineTwo: undefined,
  zip: 55555,
};

Naming is hard and I don't think OnlyKeys is a great name (it acts sort of in reverse - only keys to which U are assignable are included... which feels backwards but it's what's needed to do this).

twavv avatar Jun 15 '19 08:06 twavv

Hi, everyone!

Just found some workaround, but need to change type from undefined to void maybe it could help someone (when it's possible to change types):

function foo(arg: object | void) { }

foo(undefined)
foo()
foo({})

even works with generics, like:

type Voidable<T> = T | void;

function baz<T>(arg: Voidable<T>) { }

type T3 = { foo: string }

baz<T3>(undefined)
baz<T3>()
baz<T3>({ foo: '' })

But have some error with more complex example: link

maybe someone from TS team can help with this? 🙏

iamolegga avatar Oct 29 '19 07:10 iamolegga

@iamolegga You are having the same issue as this #29131. If you have generics, void parameters are not treated as optional. This is due to the fact that arity is checked before generic type inference. (I tried to take a stab at a PR to fix this, but changing this is likely to have a large perf impact as far as I recall)

dragomirtitian avatar Oct 29 '19 08:10 dragomirtitian

I stumbled upon this while writing some code similar to the following. I'm gonna copy-paste it here as an additional motivating example.

// Motivating example

/**
 * Creates a `fetch` function which includes headers based on some data
 */
function createFetch<D = undefined>(getHeaders: (data: D) => HeadersInit) {
  return function (url: string, data: D) {
    return fetch(url, { headers: getHeaders(data) })
  }
}

// usage with data

const fetchWithAuth =
  createFetch<{ accessToken: string }>(data => ({
    "Accept": "application/json",
    "Authorization": `Bearer ${data.accessToken}`,
  }))

fetchWithAuth("/users/me", { accessToken: "foo" }) // ok

// usage when data is undefined (default)

const simpleFetch = createFetch(() => ({
  "Accept": "application/json",
}))

simpleFetch("/users/123") // error
simpleFetch("/users/123", undefined) // ok

Output
"use strict";
// Motivating example
/**
 * Creates a `fetch` function which includes headers based on some data
 */
function createFetch(getHeaders) {
    return function (url, data) {
        return fetch(url, { headers: getHeaders(data) });
    };
}
// usage with data
const fetchWithAuth = createFetch(data => ({
    "Accept": "application/json",
    "Authorization": `Bearer ${data.accessToken}`,
}));
fetchWithAuth("/users/me", { accessToken: "foo" }); // ok
// usage when data is undefined (default)
const simpleFetch = createFetch(() => ({
    "Accept": "application/json",
}));
simpleFetch("/users/123"); // error
simpleFetch("/users/123", undefined); // ok

Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

phaux avatar Apr 01 '20 22:04 phaux

@phaux your example works if you just use void instead.. playground

lonewarrior556 avatar Apr 23 '20 03:04 lonewarrior556

@lonewarrior556 Okaaaayyy... Thanks. But now I'm only even more confused about the differences between void and undefined 😅 I thought it only makes a difference as a return type.

phaux avatar Apr 25 '20 00:04 phaux

Yea... Anyone know what version void got released with? I Want to read the release notes to see if that's intentional..

in the mean time..


const foo1 = (a: string | void) => { }
const foo2 = <T>(a: T) => { }
const foo3 = <T>(a: T | void) => { }
const foo4 = <T>(...a: [T]) => { }
const foo5 = <T>(...a: [T | void]) => { }
const foo6 = <T extends any[]>(...a: T) => { }

foo1() // work
foo2<void>() // nope
foo2<[void]>() // nope
foo3<string>() // works
foo4<string>() // nope
foo4<void>() // nope
foo5<string>() //works

foo6<[string | void]>() // works!

playground

lonewarrior556 avatar Apr 25 '20 11:04 lonewarrior556

Anyway this can be used (abused?) to allow for making generics where the arg is optional if the object contains no required properties...

type AllArgsOptional<T> = Partial<T> extends T ? void : T

type Options = {
  a: number
}

type PartialOptions = Partial<Options>


const foo1 = (a: Options | AllArgsOptional<Options>) => { }
const foo2 = (a: PartialOptions | AllArgsOptional<PartialOptions>) => { }
const foo3 = <T>(a: T | AllArgsOptional<T>) => { }
const foo4 = <T extends any[]>(...a: T) => { }


foo1() // a required
foo1({ a: 1 }) //ok

foo2() //ok
foo2({a: 1}) //ok

foo3<PartialOptions>() //Dang it!
foo3<PartialOptions>({a: 1}) //ok

foo4<[PartialOptions | AllArgsOptional<PartialOptions>]>() //ok
foo4<[PartialOptions | AllArgsOptional<PartialOptions>]>({ a: 1 }) //ok

type MakeOptionalIfOptional<T> = [T | AllArgsOptional<T>]

foo4<MakeOptionalIfOptional<PartialOptions>>() // tada!
foo4<MakeOptionalIfOptional<Options>>() // still required :)

playground

lonewarrior556 avatar Apr 25 '20 11:04 lonewarrior556

Any update on this issue as of TypeScript 4.1.2?

georgekrax avatar Dec 08 '20 09:12 georgekrax

One more solution for making undefined fields optional:

type KeysOfType<T, SelectedType> = {
  [key in keyof T]: SelectedType extends T[key] ? key : never
}[keyof T];

type Optional<T> = Partial<Pick<T, KeysOfType<T, undefined>>>;

type Required<T> = Omit<T, KeysOfType<T, undefined>>;

export type OptionalUndefined<T> = Optional<T> & Required<T>;

Playground

DScheglov avatar Jan 12 '21 09:01 DScheglov

Thank you!

georgekrax avatar Jan 12 '21 10:01 georgekrax

Mental note for myself and maybe others:

When this is available and working with inferred arguments we will be able to typecast return types depending on inferred generics 🥳

demo in TS playground

kylemh avatar Jan 28 '21 07:01 kylemh

This would be really helpful to make non-breaking changes to APIs when introducing generics.

Edit: Actually, I realized (after re-reading some of the Playgrounds above) that what I wanted was to use void instead of undefined, which worked in my simple case.

PanopticaRising avatar Mar 05 '21 05:03 PanopticaRising

I'm dealing with two classes, one where the arg for every function is required, and one where it's optional. I'd like to be able to not copy paste them, they are otherwise the same.

class Base<ARG> {
  public foo(arg: ARG){}
  public bar(arg: ARG){}
  public qux(arg: ARG){}
}

class AllRequired extends Base<string> { }
class AllOptional extends Base<string | void> { }. // <--- this doesn't make the arg optional

I can see why specifying undefined is different from an optional arg... kind of.

But why doesn't void equate to optional? They seem to be saying the exact same thing: nothing was put there.


For my case, I found a way around:

class Base<ARG extends Array> {
  public foo(...arg: ARG){}
  public bar(...arg: ARG){}
  public qux(...arg: ARG){}
}

class AllRequired extends Base<[string]> { }
class AllOptional extends Base<[string] | []> { }

const req = new AllRequired();
req.foo(); // without an arg, this will throw an error

const opt = new AllOptional();
req.opt(); // this is fine though

SephReed avatar May 14 '21 04:05 SephReed

Hey, @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){}
}

DScheglov avatar May 14 '21 11:05 DScheglov

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

ericwooley avatar May 16 '21 16:05 ericwooley