TypeScript: Treating `undefined` parameters as optional

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

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 75
  • Comments: 40 (13 by maintainers)

Commits related to this issue

Most upvoted comments

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

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

There is nothing specific to generics here. for example:

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

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

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
}

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

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

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

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

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<number>() – which then has a resolved synctask<number> 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.

Any update on this issue as of TypeScript 4.1.2?

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

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

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

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

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

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

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

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

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

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.

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.

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: https://github.com/microsoft/TypeScript/issues/12400#issuecomment-758523767

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

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

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.

any chance for 3.0?