Skip to content
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

[documentation] Clarify the semantics of void #20006

Closed
pelotom opened this issue Nov 14, 2017 · 42 comments
Closed

[documentation] Clarify the semantics of void #20006

pelotom opened this issue Nov 14, 2017 · 42 comments
Assignees
Labels
Spec Issues related to the TypeScript language specification

Comments

@pelotom
Copy link

pelotom commented Nov 14, 2017

In the handbook, it's stated that

void is a little like the opposite of any: the absence of having any type at all.

and

Declaring variables of type void is not useful because you can only assign undefined or null to them

The latter statement is not true in any useful sense; null is not assignable to void with --strictNullChecks enabled, i.e. it's no more assignable to void than any other type, so might as well not mention it.

The former is (maybe?) approximately true, but how about saying exactly what it means? I for one have never been 100% sure and would love to have some clarity. For instance, the unknown type,

type unknown = {} | undefined | null

is supposed to be a "top" type, i.e. any value should belong to it. And yet, void is not assignable to it.

declare const v: void
const foo: unknown = v
//    ^^^ Type 'void' is not assignable to type 'unknown'.

Why is that? According to the above, only undefined (and null, if no --strictNullChecks) inhabit void, so why is it not assignable to unknown?

Since there is no longer a current language specification, it seems the best available documentation is to be found at https://www.typescriptlang.org/docs/home.html, so it would be great if it could give a crisp definition of void!

@mhegazy mhegazy added the Spec Issues related to the TypeScript language specification label Nov 14, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Nov 14, 2017

We need to update the spec. but in general, the only useful location for a void type is in a return type position. there is special handling in relationship checking that allows any non-void function to be assignable to one that returns void. using it in for a variable or a parameter is wrong. you should use undefined if this is your intention.

@pelotom
Copy link
Author

pelotom commented Nov 14, 2017

@mhegazy

the only useful location for a void type is in a return type position. there is special handling in relationship checking that allows any non-void function to be assignable to one that returns void.

I'm curious how this is advantageous. Is there an example where the special semantics of void helps avoid bugs? It seems like something that might've been useful before we could express a true top type like unknown. For example, with --strictFunctionTypes any nullary function is assignable to () => unknown, yet nothing useful can be done with the result without further inspecting it.

using it in for a variable or a parameter is wrong. you should use undefined if this is your intention.

If it's wrong, why is it possible to do without error? And why is it wrong? Like any, it seems to be less of a type and more of a special pragma to the compiler; unlike any its special behavior doesn't seem to be of any practical use (any more).

@zpdDG4gta8XKpMCd
Copy link

just add void to your top type: type unknown = {} | undefined | null | void

@mhegazy
Copy link
Contributor

mhegazy commented Nov 14, 2017

One thing to keep in mind is that void existed before --strictNullChecks. all types had undefined in their domain by default.

It seems wrong to allow assigning a function that returns void to a function that returns number, just because number has undefined implicitly in its domain. So this does catch bugs for sure..

I do not think it should be an error to use void. it just does not make much sense.. similar to number & string, or never [].. they are types that will not exist at runtime, and reasoning about their behavior in such contexts is not very useful.

I do agree that under --strictNullChecks void should be assignable to {} | undefined | null in principal with {} | undefined | null being the top type and all.. but it is rather strange to be able to do something like var x: number | undefined = voidFunction(); the pattern seems likely to be a mistake, rather than an intended behavior.

I think void has a very meaningful use in a function return type.. it does say more than undefined; it is the absence of a return value. Though, at run-time that is materialized as undefined, it should mean something more at design time.. you should be able to ignore the return value safely, and you should not be able to assign it to anything that accepts | undefined, just because it looks like undefined.

@pelotom
Copy link
Author

pelotom commented Nov 14, 2017

I do agree that under --strictNullChecks void should be assignable to {} | undefined | null in principal with {} | undefined | null being the top type and all.. but it is rather strange to be able to do something like var x: number | undefined = voidFunction(); the pattern seems likely to be a mistake, rather than an intended behavior.

I'm not advocating that

var x: number | undefined = voidFunction();

should ever work! Only that

var x: {} | undefined | null = voidFunction();

should work. I'm unable to imagine a way in which this could be abused or lead inadvertently to bugs.

I also think that there's no harm in saying

const x: void = 3

How could that possibly lead to problems? You can't actually do anything with such an x, without further inspecting it, so it's harmless. It seems like the current answer is, "well it's probably not something you'd ever want to do in a real program, so we should outlaw it." But treating void as a special non-type with weird semantics both increases complexity of the type system as well as misses an opportunity to make it more useful: void could actually be the top type, i.e. it could be treated as simply equivalent to {} | undefined | null. And having a true top type would be very useful, for reasons discussed in #10715.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Nov 14, 2017

How could that possibly lead to problems?

by accident? by refactoring?

const x: void = somePromise;

the assignment above is as good as an expression statement which is a disaster for the value that cannot be thrown away:

void $.ajax.get('google.com');

callbacks with return void this is a curse on our project, because they silently swallow values that must not be lost, by allowing void to be assigned from anything we open the door to more situations like that: #8581, #8584

@mhegazy
Copy link
Contributor

mhegazy commented Nov 14, 2017

I also think that there's no harm in saying

const x: void = 3

What did you mean here by void? did you mean any? or did you mean undefined.. i am not sure i understand what x is meant to be (that could be because i have a mental association of void with return types of functions). on any rate, i would argue it is clearer to either make it undefined or {} | undefined.

But treating void as a special non-type with weird semantics both increases complexity of the type system

I think this is a subjective issue.. there are trade offs for all these design decisions. we made a decision to make void different from undefined, and i think overall it catches more wrong code than it disallows valid ones. but again this is my personal opinion, and by no means definitive.

misses an opportunity to make it more useful: void could actually be the top type, i.e. it could be treated as simply equivalent to {} | undefined | null.

I think this wrong. void has a single value at run time, it is undefined.. why would undefined be treated as a {} | undefined | null that is just not right.. the difference here is in the intent..
Again, the value of void is clarifying intent. a function that lacks a return value, for one that returns the value undefined. trying to reuse/overload/generalize void for other uses is what causes all this confusion.

@pelotom
Copy link
Author

pelotom commented Nov 14, 2017

I also think that there's no harm in saying

const x: void = 3

What did you mean here by void? did you mean any? or did you mean undefined.. i am not sure i understand what x is meant to be (that could be because i have a mental association of void with return types of functions).

That's the whole problem; it's not known what is meant by void currently because it has no clear semantics! I'm arguing that I want it to mean {} | undefined | null, because this is a useful type to have in many diverse situations.

I think this wrong. void has a single value at run time, it is undefined.. why would undefined be treated as a {} | undefined | null that is just not right.. the difference here is in the intent..

But that's not true. A void-returning function can return anything:

const foo: () => void = () => 3

so at runtime one has to expect that a void value could be anything. That's why it makes sense to consider it equivalent to {} | undefined | null.

@zpdDG4gta8XKpMCd
Copy link

what is the practical benefit of making void the "official" top type at the price of rewriting the spec and tons of code everywhere? why can you or anyone be happy with a homemade unknown = {} .... which is good for the purpose?

@zpdDG4gta8XKpMCd
Copy link

void returning function can return anything

this is wrong, try it:

function f(): void {
    return 3;
}

read closely my example the difference is subtle

@mhegazy
Copy link
Contributor

mhegazy commented Nov 15, 2017

That's the whole problem; it's not known what is meant by void currently because it has no clear semantics! I'm arguing that I want it to mean {} | undefined | null, because this is a useful type to have in many diverse situations.

It has a meaning.. a lack of a return value.. you want it to be the top type, but it is not.. :)

@mhegazy
Copy link
Contributor

mhegazy commented Nov 15, 2017

But that's not true. A void-returning function can return anything:

No. a void-returning function accepts an assignment from a function that can return anything.. since it is ok to ignore return values if they exist. but a a void-return function as authored will always return undefined..

@pelotom
Copy link
Author

pelotom commented Nov 15, 2017

No. a void-returning function accepts an assignment from a function that can return anything.. since it is ok to ignore return values if they exist. but a a void-return function as authored will always return undefined..

If all I know is that I have an f: () => void, it may have been created by

const f: () => void = () => 3;

in which case it will most definitely not return undefined. Likewise then, if all I know is I have an x: void, it may have been obtained from just such an f:

const x = f();

So, without using any casts to subvert the type system, the void type contains potentially any value at runtime. This is why I believe it should have the semantics of a top type, which means bringing its assignability relative to other types in line with this fact.

At the very least can we agree that this should work?

const foo: {} | undefined | null = voidFunction();

@zpdDG4gta8XKpMCd
Copy link

At the very least can we agree that this should work?

no we cant, void doesnt have values, so there no way to get a value of the type undefined from void, because void is an empty set, there is nothing there

(however you can shove an undefined value into an empty set called void and it will still be an empty set)

@pelotom
Copy link
Author

pelotom commented Nov 15, 2017

@Aleksey-Bykov never is the only empty set; the only type that is truly uninhabited, barring subversions of the type system. Every other type contains at least one value. undefined contains undefined, null contains null, and void contains every single value. You can claim otherwise, but you are demonstrably wrong.

@pelotom
Copy link
Author

pelotom commented Nov 15, 2017

(Well, I shouldn't say it's the only empty type... there's plenty of others, e.g. <A>() => A.)

@zpdDG4gta8XKpMCd
Copy link

in you miraculous world the type void contains every single value, here on earth void doesnt have a value, but it can swallow undefined and still be empty

the fact that you are allowed to declare a variable of void is a lie that sometimes comes handy just like another lie about never which is truly uninhabited:

declare var x: void;
declare var y: never;

@zpdDG4gta8XKpMCd
Copy link

typescript lies about types thats in its nature, its his strong and weak side, c# cant lie about types, and its very dumb and clumsy

@zpdDG4gta8XKpMCd
Copy link

now the contradiction of having a variable of the never (or void for this matter) type can be easily solved by disallowing the never type from being written/expressed/referred, so never type still exists but you cannot use it to declare a variable or a property because literal never is invalid syntax

back in a day both null and undefined types were like that in TS: although the type existed in the internals of TS, there was no way to declare a value of it, because the type didn't have any syntax

point is , for any true empty type, there must be no way to declare a value of it, hence neither void nor never are trully empty

@zpdDG4gta8XKpMCd
Copy link

couple more thoughts in that direction: #4183 (comment)

@pelotom
Copy link
Author

pelotom commented Nov 15, 2017

now the contradiction of having a variable of the never (or void for this matter) type can be easily solved by disallowing the never type from being written/expressed/referred, so never type still exists but you cannot use it to declare a variable or a property because literal never is invalid syntax

This is a bad idea. The never type is quite useful to be able to use explicitly, particularly in the type algebra of & and | to construct cool types like Diff, Omit, etc. This is the value of having true top and bottom types; even though they might seem useless on their own, they are essential building blocks from which all kinds of useful types can be built.

typescript lies about types thats in its nature, its his strong and weak side, c# cant lie about types, and its very dumb and clumsy

Yes, TypeScript has plenty of ways to subvert the type system, but this is not something to be celebrated. If there is an opportunity to remove one of these lies and increase the logic and symmetry of the type hierarchy without decreasing expressiveness, it's worth doing IMO.

@RyanCavanaugh
Copy link
Member

At the very least can we agree that this should work?
const foo: {} | undefined | null = voidFunction();

I disagree. I wouldn't want this to be legal:

// Takes any value
function myStringify(x: {} | undefined | null) { ... }
// a value of type 'void' shouldn't be used where a 'real' value is expected
myStringify(voidFunction());

I think the relation to never is instructive. A value of type never cannot be observed - a program that has observed a never value has broken the type system in some way.

A value of type void can be observed, but its value should never be used, precisely because of the return type aliasing you mentioned. A return type of void means "no one should ever look at my return value".

If it's wrong, why is it possible to do without error? /
I also think that there's no harm in saying
const x: void = 3

I think it was disallowed to even declare variables of type void for a while specifically so people wouldn't try to write weird nonsense, but we relaxed it because you could always end up with one anyway through generic instantiation.

I get the desire to have a top type for the purposes of input positions (though am still unclear on why any doesn't suit that purpose). But void's property of non-usability is an important aspect of its behavior that doesn't suit it to being a true top type.

@pelotom
Copy link
Author

pelotom commented Nov 15, 2017

@RyanCavanaugh

I disagree. I wouldn't want this to be legal:

// Takes any value
function myStringify(x: {} | undefined | null) { ... }
// a value of type 'void' shouldn't be used where a 'real' value is expected
myStringify(voidFunction());

Ok, but why not? I just fail to see what could go wrong. The function has declared itself capable of handling literally any value, and the type system would enforce that it made appropriate checks before treating it like anything more specific. What's gained by forbidding this?

I think the relation to never is instructive. A value of type never cannot be observed - a program that has observed a never value has broken the type system in some way.
A value of type void can be observed, but its value should never be used, precisely because of the return type aliasing you mentioned. A return type of void means "no one should ever look at my return value".

This is a good way of describing it, and (getting back on topic for this issue) might be a good starting point for better documentation.

I think I understand the intended semantics of void now, however much I might find it to be a wart on the type system. So thanks all for the discussion.

@zpdDG4gta8XKpMCd
Copy link

i disagree with ryan, what is observed? what is instructive? these words imply some heurstic / protocols / rituals (approved or disapproved by the mighty and only TS authority terrible but just)

really?

at the end of the day these types either just serve their purposes where they make sense or kept on the shelf when they dont

never has a few places of applicability, so does void

anyone is free and welcome to abuse them as much as they want as long as it makes them happy

any think mysterious and sacral meanings only confuse people more

@zpdDG4gta8XKpMCd
Copy link

practically though type void only makes sense where its unique properties are beneficial for solving a problem

same with never

these types are only as good or bad as you can make use of them thank to their properties, there is nothing else to it

@RyanCavanaugh
Copy link
Member

Ok, but why not? I just fail to see what could go wrong. The function has declared itself capable of handling literally any value, and the type system would enforce that it made appropriate checks before treating it like anything more specific. What's gained by forbidding this?

The fact that JS forces a return value to appear behind the scenes is a rather unfortunate result of its design that I think is beneficial to be able to ignore, much like most of our type system.

It's instructive to see how C treats void and void* - you can't capture the result of a call of a void function in any way, and you can't dereference a void*. Peeling back the curtain and unpretending that the return value doesn't exist just creates more problems where you accidently think a function returns a (useful) value but doesn't.

@pelotom
Copy link
Author

pelotom commented Nov 15, 2017

The fact that JS forces a return value to appear behind the scenes is a rather unfortunate result of its design that I think is beneficial to be able to ignore, much like most of our type system.

My understanding is that the main goal of TypeScript is to model the actual semantics of JavaScript (even the ones that may have been odd design choices) while preventing bugs. Again I ask, what could go wrong here?

It's instructive to see how C treats void and void* - you can't capture the result of a call of a void function in any way, and you can't dereference a void*.

That's not an apt comparison, because those are the semantics of C, not JavaScript. Dereferencing void* has no meaning; it does not give you a "value" that you can further inspect to see what it "really is". JavaScript references always point to a value of some definite runtime type, and you can always inspect that value to see what the type is. In C you might have a void* which points at some meaningless place in memory, and there are no operations you can perform on such a value to safely interrogate its runtime structure (since it has none).

Peeling back the curtain and unpretending that the return value doesn't exist just creates more problems where you accidently think a function returns a (useful) value but doesn't.

How could you think it returns something useful? If the return type means "this could be literally anything" that's tantamount to saying that the result is meaningless. But saying that you can't subsequently inspect that value, however meaningless it might be, is not faithful to the semantics of JavaScript. And it leads to a wart of a type in void whose treatment in the type system is an inconsistent hodge podge of special cases.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 16, 2017

if the return type means "this could be literally anything"

I think this is the point of contention here. I am saying it means this does not return any thing. which is qualitatively different than this can be anything. if that is what you want, then use {} | null | undefined

@simonbuchan
Copy link

Result callbacks are a real-world case where it's meaningful to end up in situations where you have a void argument:

type Callback<R> = (err?: any, result?: R) => void;
type Handler<A, R> = (arg: A, context: HandlerContext, cb: Callback<R>) => void;
type SomeHandler = Handler<SomeArg, void>;
// lots of other *Handler types

const case1: SomeHandler = (arg, context, cb) => {
  ...;
  cb(null, { ignored: true }); // ignored, should probably error
};
const case2: SomeHandler = wrapAsyncHandler(async (arg, context) => {
  ...;
  return { ignored: true }; // ignored, should still probably error.
});
const asyncSomeHandler = async (arg: SomeArg, context: HandlerContext) => { // note result type is inferred.
  ...;
  return { ignored: true }; // will end up ignored, but shouldn't error.
};
const case3: SomeHandler = wrapAsyncHandler(asyncSomeHandler); // the same as case2, but should have no error when ignoring the result
const case4: SomeHandler = (arg, context, cb) => {
  asyncSomeHandler(arg, context).then(result => {
    cb(null, result); // basically the same as case3, but explicitly passing void to void - could be due to the handler declaration updating, refactoring, copy-paste. Is this an error?
  });
};

function wrapAsyncHandler<A, R>(
  asyncHandler: (arg: A, context: SomeContext) => R | Promise<R>,
): Handler<A, R> {
  return (arg, context, cb) => {
    new Promise<R>(resolve => {
      resolve(asyncHandler(arg, context)); // Could end up passing void to void, but generic, so who cares?
    }).then(
      result => cb(null, result),
      reason => cb(reason),
    );
  };
}

(This example is basically the AWS Lambda runtime environment, and I've written every one of those cases!)

Perhaps void could be thought of as the super-type of everything (for variance purposes) but not assignable from anything (except void?)

I do agree that takesVoid(returnsVoid()) is weird, but:

  • it would be nice to have a short, less confusing name for {} | null | undefined, and void is a reasonable candidate
  • in generic cases like above, being able to safely declare a void result without worrying about everything exploding is nice.

@zpdDG4gta8XKpMCd
Copy link

as of today there is no super type of anything and everything, a few month ago it was so called unknown = {} | undefined | null: #21644

if you need safer void then you should use undefined

@simonbuchan
Copy link

undefined is wrong, as you are requiring everyone to cb(null, undefined) or return undefined;, which is nonsense.

Fortunately, following the links it looks like we might get a real top type! #21368 (comment)
#10715 (comment)

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Feb 8, 2018

it's less nonsense than

type F = () => Promise<string>
type G = () => void
declare var f:F;
declare var g:G;
g = f; // no problem

@simonbuchan
Copy link

const handler: Handler<X, undefined> = () => {}; // error: 'void' is not assignable to 'undefined'. Wat?

Promise being compatible with void is only "nonsense" in the sense of Promise should be treated as "don't ignore me!". It's the same issue as just calling f() and ignoring the the promise. You're looking for a "don't ignore" annotation, a.la. rust #[must_use]

@zpdDG4gta8XKpMCd
Copy link

i am looking for basic sanity:

declare var f: Promise<string>;
declare var g: void;
g = f; // no luck

@simonbuchan
Copy link

You literally already typed that. I know what you want, and I already explained why I think void covariance isn't actually the thing you don't like. Why are you complaining about it here, and not the issue you already opened for that?

@zpdDG4gta8XKpMCd
Copy link

i did many times, so what? you had a point about undefined being nonsense, i had a point that nonsense is what make undefined a sane alternative

@zpdDG4gta8XKpMCd
Copy link

apart from void nonsense, there is no need in "must use" if you ban expression statements like in F# or statements altogether like in Haskell, which are 2 successful languages that proved being practical

and the price to pay isn't that high you can just use void ... operator to discard an unwanted value and signify your intent

as simple as it seems it is not a very popular measure because requires more typing, so a path of least resistance was taken

back to "must be" how do you propagate it through the code?

/** @must_be_used */
function fn() { return {}; }

[].map(fn); // <-- a problem or not?

@simonbuchan
Copy link

I refer you to the typescript non-goals. in particular:

  1. Apply a sound or "provably correct" type system. Instead, strike a balance between correctness and productivity.

Discussion on a must-use annotation should go in your issue #8584

@zpdDG4gta8XKpMCd
Copy link

not sure what you point is, i read the non-goals and i know how to get to the items i opened, here i question the decision being made about void, and i hope that my voice is heard by adding constructive critics to the discussion

@simonbuchan
Copy link

The points would be:

  • creating a very different language to JS is an explicit non-goal of TS - complaining about why they didn't is both pointless and annoying. You have elm if you want a more F#-ey JS.
  • comments about must-use should go in the issue about that.
  • comments here should relate to clarifying the semantics of void - in particular making the case here that result types should not convert to void is reasonable - I was requesting that you come up with cases that are not better served by whatever add a flag to disable () => void being subtype of () => a #8584 ends up being, since that issue exists regardless.

@zpdDG4gta8XKpMCd
Copy link

  1. define "very different", there is no void type in JS (there are no type annotations at all) void in TS is the sole invention of the TS design team just like the semantics behind it, with this in mind the whole question about what it does or does not to JS is poorly stated and annoying, anything that has to do with the types is a fair game
  2. again, i would not say there is a big issue about "must-use" because it is secondary to the void variance problem, can be easily linted out in typical cases, and cannot or very hard to solve in a general case, and thus indeed deserves it's own topic
  3. whatever

@simonbuchan
Copy link

First point was referring to this:

there is no need in "must use" if you ban expression statements like in F# or statements altogether like in Haskell, which are 2 successful languages that proved being practicaln

If you do want that, give Elm or https://www.npmjs.com/package/tslint-immutable#no-expression-statement a try. (I'm not being sarcastic, FP is great, just not something TS will ever enforce)

Not sure what you mean with the second and third points?

  • You will have to make the case that must-use is "secondary to the void variance problem", since that fixes the problematic case you have raised here, and this doesn't for the equivalent asyncMethod(); case.
  • must-use clearly isn't very hard to solve: it's implemented in pretty every language that has a type system! Typescript has a very powerful type system, which would make it harder, sure, and if you have specific cases that would be problematic, great! Describe them there, so it can be discussed. In particular, if you think it could be done with a linter, that would be good, tslint can give errors using type info...
  • No idea what "whatever" means?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Spec Issues related to the TypeScript language specification
Projects
None yet
Development

No branches or pull requests

6 participants