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? 🌷
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 https://github.com/acdlite/flux-standard-action/pull/45#issuecomment-261863311
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)`
@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
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
}
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
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
RxJS has the exact same problem as described by @deregtd
https://github.com/ReactiveX/rxjs/issues/2852
https://github.com/ReactiveX/rxjs/pull/3074
any chance for 3.0?
@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.
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
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 = {}
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>>;
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).
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 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)
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 your example works if you just use void instead..
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.
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!
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 :)
Any update on this issue as of TypeScript 4.1.2?
Most helpful comment
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