TypeScript Version: 3.1
Search Terms: boolean void truthy falsy if while do for not
An expression of type 'void' cannot be tested for truthiness
Code
Consider this code:
function existsAndIsCool(fileName: string) {
fs.exists(fileName, exists => {
return exists && isCool(fileName);
});
}
if (existsAndIsCool("myFile")) {
console.log("Cool!");
} else {
console.log("Not cool :(");
}
Expected behavior: The "Cool!" branch is actually unreachable because existsAndIsCool
returns void
, not boolean
. I should have been warned about this at some point.
Actual behavior: No error, never cool 😢
Playground Link: Link
Related Issues:
PR #26234
This is now an error.
Hi, I just updated my CI/CD pipeline env to 3.1.1 and I encounter this error while building docker image.
An expression of type 'void' cannot be tested for truthiness
My code is:
(err: Error) => (err ? console.log('Upload failed.') || rej(err) : console.log('Upload completed.'))
I think that using the expression, that evaluates to void, like this is perfectly legit, because as typescript docs says "Declaring variables of type void is not useful because you can only assign undefined or null to them". So if it is allowed to check both, null and undefined, in this kind of expression, also void must be permitted.
In addition to that, JS's undefined value is same as void, since JS adds anyway a "return undefined" if a functions hasn't any return value.
I think that TS here forgets that it is a JS superset and this makes me sad.
@fox1t the problem with assuming a void-returning function is going to return a falsy value is that this code is legal:
var x: () => void = () => true;
// somethingImportant does not run
if (x() || somethingImportant()) {
}
You should use the comma operator if you intend for two sequenced operations to always occur.
I think that TS here forgets that it is a JS superset and this makes me sad.
https://stackoverflow.com/questions/41750390/what-does-all-legal-javascript-is-legal-typescript-mean
@RyanCavanaugh I think that there is no need to assuming anything because in JS we have truthy and falsy values and all operators check for that. In addition to that conditional operators are short-circuited and return the value and don't operate any conversion type to boolean.
I assume, because of docs I linked, that also TS would work the same. If we "introduce real" true and false checks (I mean the boolean one and not truthy and falsy), also
const foo = (bar) => bar && bar.baz
// or
const baz = (name) => name || 'defaultName'
can be considered "illegal" because neither bar
or name
are not boolean values.
The following isn't legal code:
(err: Error) =>
err ? console.log('Upload failed.'), rej(err) : console.log('Upload completed.')
Can't use comma operator here, but || is legit. I use TS to have a better FP experience in JS, but this type of changes, moreover in minor version, are really annoying.
Anyway, just to point it out, I am here to understand and I am not blaming anyone.
Good point about me being sad about wrongly understand ts is js's "superset". :)
Does this change mean that Typescript insteads to remove boolean coercion all together? If not, what makes this type different from all other types?
If not, what makes this type different from all other types?
Giving different behavior to different types is why we have types in the first place.
The odds that you meant to write code like the sample in the OP are approximately 0%. Even if you intended to write code like voidReturningFunc() || someImportantOp()
, it's unlikely you fully understood the implication of void
- many people believe that a void
-returning function will always return a falsy value, but this is not the case.
Moreover, code like someCall() || someImportantOp()
is an extreme code smell because it is not obvious to a reader of the code that the evaluation of someImportantOp
is intended to actually be unconditional -- the comma operator should have been used here for clarity and certainty.
Giving different behavior to different types is why we have types in the first place.
Ah sorry, I thought it was obvious from context that I meant what makes void being coerced to a boolean value invalid typescript, as opposed to a string being coerced to boolean, or float or any other type.
From a purity standpoint, it doesn't make sense to coerce a string to boolean either, right? The only false string is '' and it seems more correct to use === to test that. So if you all are ratcheting up the purity of coercion in TypeScript, I don't think it's unreasonable to ask if you all are planning on making boolean coercion illegal for other types.
@kitwestneat just get the point I was trying to understand here. In this very moment there are 2 different behaviour of the language for the same operation, and it is wrong from every point of view. In addition can you @RyanCavanaugh pleas elaborate "many people believe that a void-returning function will always return a falsy value, but this is not the case"?
Also the TS docs say: "Declaring variables of type void is not useful because you can only assign undefined or null to them". So it is legit to expect to use void here, since it is the "same" as undefined and null...
It's legal to coerce a string to a boolean because a) this is idiomatic in JS, b) some strings are falsy and some strings are truthy according to rules which are more or less predictable, and c) it's easily conceivable why you might want to do some things with truthy strings and other things with falsy strings
How do you get an expression of type void
? Effectively there are two ways:
undefined
So it is legit to expect to use void here, since it is the "same" as undefined and null...
I think this has been clearly explained already: https://github.com/Microsoft/TypeScript/issues/26262#issuecomment-425976165
Ok, thanks for the explanation.
Let's look at a longer example:
function myForEach<T>(arr: T[], callback: (arg: T) => void) {
let i = 0;
// love too sequence with ||
while (i < arr.length) callback(arr[i]) || i++;
}
// Yep
myForEach([1, 2, 3], n => console.log(n));
// Let's copy this array into another array
let dst: number[] = [];
// Out of memory error, wtf?
myForEach([1, 2, 3], n => dst.push(n));
Ok, this is really a nice example! Love it.
Don't you think that the real problem is before ||
operation. What is now bothering me is:
n => dst.push(n)
has (n: any) => number
type and the callback parameter is declared as callback: (arg: T) => void)
. Isn't this already a problem on its own that I am allowed to pass a callback with wrong signature?
It's not a wrong signature; see https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-void
If you want a function returning undefined
, write () => undefined
. If you want a function whose return value you promise not to "look at", write () => void
. Two different types; two different meanings.
Basically the entire point of the void
return type in function types is to give you a safer alternative to an any
return type in the function body.
// Let's say the return type is any
function myForEach<T>(arr: T[], callback: (arg: T) => any) {
// No type error, crashes at runtime, sad
for (let i = 0; i < arr.length; i++) callback(arr[i]).argle.bargle;
}
Many people believe that a void-returning function will always return a falsy value, but this is not the case.
@RyanCavanaugh This is surprising to me, and probably many other programmers too. Can the docs be updated with some examples of this?
If you want a function returning
undefined
, write() => undefined
. If you want a function whose return value you promise not to “look at”, write() => void
. Two different types; two different meanings.
This is enlightening. I always understood void
to be interchangeable with undefined
. Again, I think clearer docs are essential.
Also, if users are expecting console.log()
to always return undefined
(which it does), doesn’t it make sense to encourage libraries to update this signature? If there were clearer direction on the difference between type void
and type undefined
then that would help alleviate a lot of misconceptions.
Something that returns void
means something like "I don't care what this returns and you should not care either". You should never look at the return value of a void
function, not even as a truthy or falsy value.
Specifying that a function that happens to return a value to be ignored specifically returns undefined
is, IMO, unnecessary hardcoding of implementation details. Returning void
is also very useful at signifying the side-effectful nature of a given function.
This is especially a problem in contravariant positions -- for example, a function argument that returns void (e.g. () => void
) is allowed to be an async
function that creates a floating Promise
, because you don't care what it returns. If it was () => undefined
, not only would that not be allowed, your argument function would have to _specifically_ return undefined
in its body.
@Kovensky That makes sense, but "I don't care what this returns and you should not care either" sounds a lot like any
.
Maybe a type hierarchy diagram would be helpful? Having unknown
at the top (the "universal type" / "union of all types") and never
at the bottom (the "empty type" / "intersection of all types"), and all the other built-in types in between. Specifically, how void
and any
fit into that hierarchy. I come from a background in propositional logic & set theory so I guess that's how I'm approaching this.
any
however is incredibly dangerous and conveys no meaning other than "this can be literally anything". Anything you do with the result is also completely out of reach of the type system.
In your chart, any
would be completely outside the chart because any any
shuts down all type safety. void
is useful as an indicator of "only called for its side effect", even if an inferred void
type is the same as undefined
at runtime.
So, wrapping up all of the great comments here, I think we can say that void
is meant to be used for functions that only have side-effects and that is different both from any
and undefined
, even if
const bar: void = undefined
is valid syntax.
There is still one question left though. As i pointed out before, official TS docs say "Declaring variables of type void is not useful because you can only assign undefined or null to them", but trying to write const foo: void = null
gives Type 'null' is not assignable to type 'void'.
error. Docs update needed?
@fox1t , I believe that docs are correct.
You have this error because you have strictNullChecks: true
(or strict: true
for that matter) in your tsconfig.json
. By default strictNullChecks
is false
, and void
is assignable from null
.
Btw, I've got a nice type compatibility table. Paste it into any editor capable of highlighting TypeScript errors and you will see which types are compatible and which are not (depending on the active tsconfig.json
).
// Type compatibility table
function fx(): never { throw Error("Never returns"); }
function fv(): void { }
var x: never = fx();
var u: undefined = undefined;
var v: void = fv();
var n: null = null;
var t: {} = {}; // random type
var a: any = {};
/*
|x |
--+-----+
y |x = y| (typeof X "assignable from" typeof Y)
--+-----+
*/
/* | never | undefined | void | null | T | any |
-----------+--------------+--------------+--------------+--------------+--------------+--------------+
never |*/ x = x; /*|*/ u = x; /*|*/ v = x; /*|*/ n = x; /*|*/ t = x; /*|*/ a = x; /*|
undefined |*/ x = u; /*|*/ u = u; /*|*/ v = u; /*|*/ n = u; /*|*/ t = u; /*|*/ a = u; /*|
void |*/ x = v; /*|*/ u = v; /*|*/ v = v; /*|*/ n = v; /*|*/ t = v; /*|*/ a = v; /*|
null |*/ x = n; /*|*/ u = n; /*|*/ v = n; /*|*/ n = n; /*|*/ t = n; /*|*/ a = n; /*|
T |*/ x = t; /*|*/ u = t; /*|*/ v = t; /*|*/ n = t; /*|*/ t = t; /*|*/ a = t; /*|
any |*/ x = a; /*|*/ u = a; /*|*/ v = a; /*|*/ n = a; /*|*/ t = a; /*|*/ a = a; /*|
-----------+--------------+--------------+--------------+--------------+--------------+--------------+
*/
// Observations (under `strict: true`):
//
// 1. `never` assignable-from only `never`
// 2. `never` assignable-to any type
// 3. `any` assignable-from any type
// 4. `any` assignable-to any type (except for `never`, see p.1)
// 5. `undefined` assignable-to `void`
// 6. `undefined`, `null`, `T` are invariant
//
Demo screenshot:
_Update_: Published this table in a dedicated repo:
https://github.com/earshinov/typescript-type-compatibility-table
@earshinov that is gorgeous 😍
Adding unknown to above :)
// Type compatibility table
function fx(): never { throw Error("Never returns"); }
function fv(): void { }
var x: never = fx();
var u: undefined = undefined;
var v: void = fv();
var n: null = null;
var t: {} = {}; // random type
var a: any = {};
var k: unknown = {};
/*
|x |
--+-----+
y |x = y| (typeof X "assignable from" typeof Y)
--+-----+
*/
/* | never | undefined | void | null | T | any | unknown |
-----------+--------------+--------------+--------------+--------------+--------------+--------------+--------------+
never |*/ x = x; /*|*/ u = x; /*|*/ v = x; /*|*/ n = x; /*|*/ t = x; /*|*/ a = x; /*|*/ k = x; /*|
undefined |*/ x = u; /*|*/ u = u; /*|*/ v = u; /*|*/ n = u; /*|*/ t = u; /*|*/ a = u; /*|*/ k = u; /*|
void |*/ x = v; /*|*/ u = v; /*|*/ v = v; /*|*/ n = v; /*|*/ t = v; /*|*/ a = v; /*|*/ k = v; /*|
null |*/ x = n; /*|*/ u = n; /*|*/ v = n; /*|*/ n = n; /*|*/ t = n; /*|*/ a = n; /*|*/ k = n; /*|
T |*/ x = t; /*|*/ u = t; /*|*/ v = t; /*|*/ n = t; /*|*/ t = t; /*|*/ a = t; /*|*/ k = t; /*|
any |*/ x = a; /*|*/ u = a; /*|*/ v = a; /*|*/ n = a; /*|*/ t = a; /*|*/ a = a; /*|*/ k = a; /*|
k |*/ x = k; /*|*/ u = k; /*|*/ v = k; /*|*/ n = k; /*|*/ t = k; /*|*/ a = k; /*|*/ k = k; /*|
-----------+--------------+--------------+--------------+--------------+--------------+--------------+--------------+
*/
// Observations (under `strict: true`):
//
// 1. `never` assignable-from only `never`
// 2. `never` assignable-to any type
// 3. `any` assignable-from any type
// 4. `any` assignable-to any type (except for `never`, see p.1)
// 5. `undefined` assignable-to `void`
// 6. `undefined`, `null`, `T` are invariant
//
Guys, is there a solution to this issue ?
Here's how it affects my project. I use swagger plugin (swagger-codegen-maven-plugin) in JAVA Spring project to generate the Angular/TS API (i.e. front-end client services and interfaces/modules). Now there's a manual step of fixing the autogenerated code, as the Angular project doesn't build due to this breaking change in TS (Use of 'void' in control flow constructs is now disallowed Error).
So, is TS going to fix this (I tested in Chrome Console JS works just fine with void evaluating to falsy) OR it is going to be a deviation from JS from now on in respect to this particular functionality ?
P.S. I also tested manual generators available, but all work probably with the same template and cannot escape the error in the generated code unless manual fixing it ... Thanks if s.o. pays attn to this
I was using the console.debug(props) ||
for quickly debugging just about everything, I even have a snippet for that. With the comma separator, it is necessary to also enclose the code within brackets.
const setFilter = (filter: FilterType) => setTheFilter(filter) // code to debug
const setFilter = (filter: FilterType) => console.log(filger) || setTheFilter(filter) // before
const setFilter = (filter: FilterType) => ( console.log(filger), setTheFilter(filter) ) // now
Can I suppress the error or can I augment the console declaration to look like it's returning a falsy value?
//EDIT
Okay, this makes the job done.
declare global {
interface Console {
debug(message?: any, ...optionalParams: any[]): undefined;
}
}
Looks like you can still coerce a void function to a number, and then do a truthiness check, so +console.log("log") || aFn()
works to avoid the outer brackets needed with the comma operator.
I apologize for bringing this up again after so much time, but I can see a couple of problems with the current state of things.
void
is not a voidIf you want a function whose return value you promise not to "look at", write
() => void
.
It is a surprise for me (and, as I can see, for other devs too) that the type void
does not actually mean a void-like value, such as undefined
or null
. This however contradicts the docs on a subject:
The quoted comment (coupled with GitHub Wiki page) implies usage of void
as some sort of indication that this value could be anything. Sort of like the any
but not, since any
could be harmful.
But since TS3.0 there exists a type, which purpose is literally to indicate unknown or unknowable values — I'm talking about unknown
of course. Introducing unknown
and _then_ changing void
to be closer to unknown
seams weird decision to me.
Is there an open discussion on meaning of void
type? I couldn't find it unfortunately. If so, I would like to join it.
void
behaves inconsistently[…] the problem with assuming a void-returning function is going to return a falsy value […]
@RyanCavanaugh, you've suggested a snippet similar to this:
const f: () => void = () => true;
// no errors
AFAICT, it doesn't produce errors due to the current purpose of void
, which is to indicate that "you shouldn't care about the value" (see the previous paragraph).
But then again, the following snippet _does_ show error, despite being very similar:
function f(): void {
return true; // Error: 'true' is not assignable to type 'void'
}
Is this a bug, or I'm missing something? I probably am. But if it is really a bug, then IMO the first snippet is erroneous, and the second one makes sense.
Both in issue title and in labels it is stated that its fix will be a breaking change:
Also, the very point of the fix is to "break" certain architecture patterns. Yet, the change was shipped in a minor update.
To my memory, that is the second time when the TypeScript's version is not compliant to SemVer, which causes things to break. The first one is shipping new, breaking defaults with 2.9
. (I personaly find the 3.3.3333
joke harmless and funny indeed, so it doesn't count.)
The latest SemVer specs suggest shipping any breaking (_sic._ "incompatible") changes _only_ in major releases. It looks like the current development process of TypeScript allows this to be done, without refusing features due to their "breakingness".
Why is void
allowed to be checked for truthiness when it's a discriminated union with another type?
(a: A | void) => {
a ? a.b : undefined // works
};
Do you think I should file a bug for this?
void
is a bad type that predates unknown
and undefined
and null
and how it works is by and large an artifact of how it _used_ to work (ie, we generally don't try to change how void
behaves - we change how other things behave in the presence of void
instead). If you're writing new type definitions today, it's almost always better to declare things of type unknown
or undefined
, depending on your intent, instead of void
(which sort of acts like either, depending on context, which is why it's so inconsistent).
This still bothers me a bit though
const f: () => void = () => true; // no errors
function f(): void {
return true; // Error: 'true' is not assignable to type 'void'
}
@RyanCavanaugh
.then(x=> console.log(x) || x)
is a type error.
How can we turn off this rule?
@stavalfi
```
.then(x => (console.log(x), x));
@stavalfi
just coerce the console.log to a not a number
.then(x=> +console.log(x) || x)
OR augment the declaration
declare global {
interface Console {
log(message?: any, ...optionalParams: any[]): undefined;
}
}
Most helpful comment
@fox1t , I believe that docs are correct.
You have this error because you have
strictNullChecks: true
(orstrict: true
for that matter) in yourtsconfig.json
. By defaultstrictNullChecks
isfalse
, andvoid
is assignable fromnull
.Btw, I've got a nice type compatibility table. Paste it into any editor capable of highlighting TypeScript errors and you will see which types are compatible and which are not (depending on the active
tsconfig.json
).Demo screenshot:
_Update_: Published this table in a dedicated repo:
https://github.com/earshinov/typescript-type-compatibility-table