microsoft / TypeScript

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

Treating `undefined` parameters as optional #12400

Open unional opened 7 years ago

unional commented 7 years ago

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

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

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

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

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

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

mhegazy commented 7 years ago

There is nothing specific to generics here. for example:

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

f(); // Not allowed
f(undefined); // OK
unional commented 7 years ago

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
}
deregtd commented 6 years ago

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

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.

RyanCavanaugh commented 6 years ago

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

felixfbecker commented 6 years ago

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

agalazis commented 6 years ago

any chance for 3.0?

sandersn commented 6 years ago

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

minajevs commented 5 years ago

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

jeremija commented 5 years ago

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 = {}
twavv commented 5 years ago

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

iamolegga commented 4 years ago

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

dragomirtitian commented 4 years ago

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

phaux commented 4 years ago

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 ```ts "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 ```json { "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

lonewarrior556 commented 4 years ago

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

phaux commented 4 years ago

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

lonewarrior556 commented 4 years ago

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

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

georgekrax commented 3 years ago

Any update on this issue as of TypeScript 4.1.2?

DScheglov commented 3 years ago

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

georgekrax commented 3 years ago

Thank you!

kylemh commented 3 years ago

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

PanopticaRising commented 3 years ago

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.

SephReed commented 3 years ago

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

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){}
}
ericwooley commented 3 years ago

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

jacob-haller-roby commented 3 years ago

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

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

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

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();
data-enabler commented 1 year ago

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.

unional commented 1 year ago

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.

data-enabler commented 1 year ago

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?

unional commented 1 year ago

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

chharvey commented 1 year ago

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
christianalfoni commented 8 months ago

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.