-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Treating undefined
parameters as optional
#12400
Comments
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)` |
* Add type guard, generic, and test * Improve generic naming * Add ErrorFSA alias * Write test correctly to avoid confusion * Add `someField` validation * Add somefield test * Add semi-colon, move tsc to posttest * Add 3 more semi-colon * Remove optional designation. Since `payload` and `meta` is generic, user can specify `void` or `undefined` to indicate it does not present. Removing the optional designation improves type guard. see `isCustomAction4()` for example * Fix more linting issue * Adding eslint for typescript. Update yarn.lock * Change typescript-eslint-parser to devDeps * Move eslint-plugin-typescirpt to devDeps also.
@RyanCavanaugh @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 |
undefined
for generic as optionalundefined
parameters as optional
undefined
parameters as optionalundefined
parameters as optional
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
} |
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. |
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. |
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 |
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 🥳 |
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 |
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.
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:
|
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){}
} |
There are lots of workarounds here, Maybe all that's really needed is some good documentation or utility types? |
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 |
I tried to use 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 , 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 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
} |
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 i.e. something like: type FSA<Payload, Meta> = Equal<Payload, void>
? { ... }
: { payload: Payload } There is a semantic difference between |
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 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 |
I disagree with treating 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 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 |
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 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. |
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? 🌷
The text was updated successfully, but these errors were encountered: