Repository navigation
Treating undefined parameters as optional #12400
Description
Activity
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?
As a counter argument, it does make sense that the property should be not optional redux-utilities/flux-standard-action#45 (comment)
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)`
Reacted by Dmitrijs Minajevs, Michael Wilson, NN, vultix, Eric Wooley, Guilherme Pacheco, Jean-Christophe Delmas, Ludovit Hajzer, JorisMolnar, Denis Kustura and 9 more- added a commit that references this issue
on Jan 30, 2017 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?
There is nothing specific to generics here. for example:
declare function f(a: string | undefined): void; f(); // Not allowed f(undefined); // OK
Reacted by Homa Wong, Ludvík Prouza, Daniel M., Etienne Dldc, Carl, SlurpTheo, Charles Samborski, Michael Wilson, Jerko Steiner, Hector Maddock-Greene and 17 more- changed the title
[-]Accepting `undefined` for generic as optional[/-][+]Treeting `undefined` parameters as optional[/+]on Feb 3, 2017 - addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Feb 3, 2017 - changed the title
[-]Treeting `undefined` parameters as optional[/-][+]Treating `undefined` parameters as optional[/+]on Feb 12, 2017 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 }
Reacted by Erik Müller, Shayan Sayahi, Arseny Smoogly, Wade Peterson, Jason Kurian, Jarrod Mosen, Rhys van der Waerden, Charles Samborski, Ryan Smith, Rune Mehlsen and 9 moreAdding 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.
Reacted by Wade Peterson, Alexey Morozov, Felix Becker, Hector Maddock-Greene and PanopticaRisingReacted by Brian KimReacted by Mingwei SamuelTo 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.
Reacted by Felix Becker and Hector Maddock-Greene- addedCommittedThe team has roadmapped this issueThe team has roadmapped this issueand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Nov 28, 2017 25 remaining items
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){} }
There are lots of workarounds here, Maybe all that's really needed is some good documentation or utility types?
Reacted by Seph Reed and Mikhail Priverjacob-haller-roby commented
on Jul 1, 2021 More actionsTrivial 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
I tried to use
voidwith conditional types, but i still need to passundefinedas 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 }
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
ActionContextgeneric 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 }
Reacted by freakzlikeA 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();
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.
After 6 years since I raise this issue, I realized that what I really want is the other way around. Instead of "treating
undefinedas 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
undefinedandoptional.
I don't want this suggestion makes them even more muddy.There is a semantic difference between
undefinedandoptional. 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?
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
voidand what isnever? Doesvoidmaps tounitin Haskell andnevermaps toVoidin 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
Reacted by Dmytro ShchehlovI disagree with treating
undefinedparameters 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
exactOptionalPropertyTypescompiler option. Perhaps add a newexactOptionalParameterTypesoption?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
on Jan 9, 2024 More actionsHi 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
valueproperty. The typing reflects thevalueof the signal, though you can pass in aninitialValueas 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.
Reacted by Homa Wong and Artur MollaievReacted by Artur Mollaievworkaround:
/** 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>) {}
Reacted by Dmytro Shchehlov, SancheZz and Xandor SchieferNiki (@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');
Reacted by Niki and strblr
tsc: 2.2.0-dev.20161116
Discover this behavior from redux-utilities/flux-standard-action@78a9065
This is the original code:
However, this does not work well with type guard:
Since generic type can be anything, including
undefined, I'm considering to remove the optional designation:However, now the creation code fail:
The workaround is to depends on infer type:
or specify it:
Is there a better solution? 🌷