-
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
[Request for feedback] Nullable types, null
and undefined
#7426
Comments
I thought that Belief basis:From #7140 That PR has instances in the discussion where it means only undefined and sometimes both null and undefined. I think Nullable should mean one of these consistently everywhere. Sorry if I've misread 🌹. |
Just to explore what this means Option 1:
|
@basarat don't take the language in the PR as gospel -- earlier comments came before we actually tried out that behavior in the compiler (which, as a data point, does not use |
As @RyanCavanaugh, this issue is to illicit feedback to put in #7140; so #7140 should not be used as the expected behavior yet. |
My vote: Allowing Reasons:
@RyanCavanaugh looking back I did originally misread the question here 🌹 :) |
From a developer ergonomics perspective, 99% of the time I care about nullability it's because I want to know "is it safe to call So for me it comes down to: will the nullable type support help the compiler prevent me from doing the following mistake? let x = document.getElementById('nonexistent-id');
x.innerText = 'hello'; Unfortunately, it appears that x is Finally, based on my "99% of use cases" metric, the proposal variant involving ?? makes the language a lot uglier for not a proportional amount of gain. Regarding optionality and confusingness (the symmetry arguments), I don't mind asymmetries because the two question marks in |
Another use case to consider: let map: {[key: string]: MyObject?} = ...;
let x = map['foo'];
x.bar(); |
I vote for option 1 as described by @mhegazy but I'd like a clarification. Are there any problems with the lack of symmetry between
There is a 3rd signature: (x?: number?) which would be
And finally we add the following quirk of JavaScript: passing
Thats the intuition, anyways. This also makes it possible to model functions that only really expect optional arguments but never nulls. Unfortunately, this would mean some type definitions may need to be updated to allow for nulls. Its hard to say whether On the other hand, I imagine that old style It would probably be a good idea to look at some utility libraries (lodash, jquery, etc) and see what kind of checking functions they provide, as well as how they are used in the wild (*) of course its distinguishable, however in practice most code doesn't or shouldn't make a distinction. Unfortunately I cannot remember the esdiscuss thread that argued this... |
In case someone finds this useful there's a similar proposal for the dart language here (Non-null Types and Non-null By Default (NNBD)). It includes a comparison to null types in other languages. |
I prefer In any case, having base types which include neither value lets me create more accurate typings, this is just a contest for shorthand syntax. |
TL;DR: Flow was right, Let me put forth a new option: Unifying around the idea that Motivating example 1: Functions function fn(x: number?) { /* implementation here */ }
fn(undefined); // OK
fn(null); // Error
fn(); // Error This is very much wrong. Outside of dodgy stuff like checking Let's consider instead that function fn(x: number?) { /* implementation here */ }
fn(undefined); // Error, x is missing
fn(null); // OK
fn(); // Error, x is missing Now the cases where Then consider the optional parameter case: // Callers see this as (x?: number) => void.
// *Not* equivalent to (x: number?) => void
function fn(x = 4) {
// No need for null/undef checks here
// since x can't be null
console.log(x.toFixed());
}
fn(undefined); // Allowed
fn(null); // Error, can't assign null to number
fn(); // Allowed This behavior is the same as Option 1 above, except that we don't need to think about how to introduce Combining the two: function fn(x: number? = 4) {
// Must check for 'null' in this body
if(x === null) {
console.log('nope');
} else {
console.log(x.toFixed());
}
}
// All OK, of course
fn();
fn(null);
fn(3);
fn(undefined); Motivating Example 2: Objects interface Point {
x: number?;
}
var a: Point = { x: undefined }; Should this be legal? If we asked a JS dev to code up an function isPoint(a: Point) {
// Check for 'x' property
return a.x !== undefined;
} Again, by saying Separating concerns and
The first concern, existence or initialization, is checked by using The second concern, having a value, is conditional on existence/initialization. Only things that exist and aren't It's a mistake to try to merge these concepts into one unified thing. See the section on For an arbitrary
Given this:
We can answer this by writing two declarations: function alpha(x?: number) { }
function beta(x: number?) { } If we believe in separation of concerns, as I think we should, then:
In other words,
One thing to notice in the compiler implementation is that we actually have lots of non-values that are implemented as sentinel references that could have been Conversely, we use For example, this code uses const arg = getEffectiveArgument(node, args, i);
// If the effective argument is 'undefined', then it is an argument that is present but is synthetic.
if (arg === undefined || arg.kind !== SyntaxKind.OmittedExpression) { The implementation is: function getEffectiveArgument(node: CallLikeExpression, args: Expression[], argIndex: number) {
// For a decorator or the first argument of a tagged template expression we return undefined.
if (node.kind === SyntaxKind.Decorator ||
(argIndex === 0 && node.kind === SyntaxKind.TaggedTemplateExpression)) {
return undefined;
}
return args[argIndex];
} Where's the bounds checking in that code? It's in the calling code: const argCount = getEffectiveArgumentCount(node, args, signature);
for (let i = 0; i < argCount; i++) {
const arg = getEffectiveArgument(node, args, i); How do we know that |
@RyanCavanaugh That makes a great deal of sense. 👍 ( Also, from a purely DX perspective, if function func1(params: {foo: string, bar: string?}) {
// ...
}
function func2(obj: Thing) {
let foo = obj.getFoo()
let bar = obj.getBar()
// ... use foo and bar ...
func1({
foo: foo
// oops, forgot to pass bar
})
}
The page @basarat linked to (http://flowtype.org/docs/nullable-types.html) says Flow considers |
@RyanCavanaugh This proposal makes sense to me. Feels like the best we can do in a language that supports both One clarification on object properties... let's take an interface... interface Foo {
w: string?,
x?: string,
y?: string?,
z: string??
} Are these assumptions correct? #0 (can't get Markdown to play nice here) This code can also be written as: interface Foo {
w: string | null,
x: string | undefined,
y: string | null | undefined,
z: string | null | undefined
}
|
is this so important to come up with the consistent syntax? let's face it there are 3 cases to address:
and then what about this:
can we just keep it the way it is:
if you ask me the the argument that
|
If you want another example of prior work other than Flow, in JSDoc On Tue, Mar 8, 2016, 9:57 AM Aleksey Bykov [email protected] wrote:
|
I prefer solution 1. Though, I wonder if anyone have considered separating the meaning of
Lets begin with explaining the latter For
C++ also default initializes to undefined behaviour and not Also quoting one of the comments of the first answer in the SO thread:
One of the downside, is that we must deal with how to handle uninitialized variables today: let s: string;// string | undefined The proposed syntax is: let s?: string;// string | undefined So there is quite a LOT of code that must be rewritten. BUT probably as many lines as any non-null proposal. Also one can define let s?: string?;// string | null | undefined One other downside is, there might be a few people who have written function that returns function getString(): string | undefined {
if (Date.now() % 2 > 0) {
return 'hello world';
}
return undefined;
} Pros
Cons
|
there is one more problem with the current semantics of
and it's a huge PITA, because turns out it's not the same as |
A few assertions to ground this discussion:
The purpose of this discussion is only to debate which of the following shorthand notations we want:
In other words, we're bikeshedding on the meaning of the Option 1 is where we're currently at, but we were simply curious about people's opinions on 2 and 3. I should mention that it is of course always possible to define your own preferred type notation using generic type aliases. For example: type Opt<T> = T | undefined;
type Nullable<T> = T | null; |
To summarize @RyanCavanaugh proposal in the same terms:
|
you forgot the 3rd case: when I need |
@Aleksey-Bykov as @ahejlsberg mentioned, |
Ok, so we now have five options:
|
you cannot put 3 pigeons in 1 cage, i vote for 1: although can't see what sort of a well supported argument can be made here |
Can we add option 6, have a modifier for both?:
|
Do we really need
instead, and in record fields you can use
instead. Which AFAICT means the only place where a For that case, could we not use the following syntax? var x?:number?; Reads: Local variable Another advantage is that the sugar also works for a local variable that can be undefined (but not null): var x?:number; Unless I'm forgetting something? |
@spion relying on |
@spion |
@weswigham / @Aleksey-Bykov yeah, it seems I forgot about casting. edit: examples of casting / standalone types would be useful - I can't think of any realistic ones. |
chore(TypeScript): Enable 'strictNullChecks' option This tries to enable [`--strictNullChecks` option](microsoft/TypeScript#7140) of TypeScript compiler. - [Non-nullable types by ahejlsberg · Pull Request #7140 · Microsoft/TypeScript](microsoft/TypeScript#7140) - [Non-strict type checking · Issue #7489 · Microsoft/TypeScript](microsoft/TypeScript#7489) - [[Request for feedback] Nullable types, `null` and `undefined` · Issue #7426 · Microsoft/TypeScript](microsoft/TypeScript#7426) - [Control flow based type analysis by ahejlsberg · Pull Request #8010 · Microsoft/TypeScript](microsoft/TypeScript#8010) <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="35" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/karen-irc/karen/604) <!-- Reviewable:end -->
typeof null === 'object' as Douglas Crockford noted, this is plain wrong. Elaborating, he alluded to a dialog with Brendan Eich where the latter stated that this was a bug that was never fixed (citation not provided). That is definitely a reason not to use |
Definitely? That's radical and it seems many people use BTW you will not be able to completely evade Haskell and Scala (and most languages) don't have both a |
@jods4 I disagree, the absence of a property does not indicate the need for a separate value because the equivalent of a JavaScript object in a language like Java is a hashtable that returns |
*typo (I think). You meant to say |
the problem with both undefined and null is that in practice there might be
and worse comes to worst next time the same values camean completely now pardon me, what exactly are we discussing here?
|
Consider if in a statically typed language you have a
If
This is why When it comes to |
@> *typo (I think). You meant to say undefined. @basarat I actually did mean My point was exactly what you went on to say
Exactly this. One shouldn't need to care. It's unfortunate that we have to deal with both, but the distinction between them is not semantically significant, and the argument that a dynamic language needs both is erroneous.
That is wrong. The fact that |
See
Also note the distinctly different wording for
The original tagged union for a JavaScript value had This way you could, say, take a Java object and ask if a property exists with
|
@jesseschalken I know the behaviour is well specified and standardized, I'm saying it's semantically wrong for null to have type of object. |
I'm using TS 2.0.7 and trying to understand how this was implemented. It seems that in TS 2.0.7, if I write:
Then the type of
However, if I do this:
The I get this:
This kind of sucks because a) it seems inconsistent because something besides the type is deciding whether it can be left out and b) if it worked I could do this:
I dug through several threads, but I didn't see any reason why Thanks. |
@xogeny If you declare function x(n?: number) {} Then the parameters On the other hand, if you declare function x(n: number | undefined) {} Then the parameter is not optional and you have to pass it, even when undefined, like so: The optionality part can be seen more clearly if you try to add more parameters. The following declaration is illegal: It works the same for optional members. 'x' in { x: undefined } === true;
'x' in { } === false;
Object.assign({x: 3}, {x: undefined}) // == {x: undefined }
Object.assign({x: 3}, { }) // == {x: 3} |
Late to the party but nevertheless ... one issue which is conspicuous by its absence in most of the above discussion is anything ado about the
This makes me wonder if the type
Accordingly could this discussion be usefully framed around the concepts of nullability versus voidability? And then consider writing:
|
|
With the work on Nullable types in #7140, we would like to field some user input on the current design proposal.
First some background:
null
andundefined
JavaScript has two ways that developers use today to denote
uninitialized
orno-value
. the two behave differently. where as null is completely left to user choice, there is no way of opting out ofundefined
, so:a
will always implicitly hasundefined
, and so will the return type ofbar
.Nullability
Given the JS semantics outlined above, what does a nullable type
T?
mean:T | null | undefined
T | undefined
1.
T | null | undefined
It is rather subtle what
?
means in different contexts:2.
T | undefined
This is more consistent, the
?
always means to| undefined
; no confusion here.T??
can be used to meanT | undefined | null
orT? | null
The text was updated successfully, but these errors were encountered: