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

[Request for feedback] Nullable types, null and undefined #7426

Closed
mhegazy opened this issue Mar 7, 2016 · 162 comments
Closed

[Request for feedback] Nullable types, null and undefined #7426

mhegazy opened this issue Mar 7, 2016 · 162 comments
Labels
Discussion Issues which may not have code impact

Comments

@mhegazy
Copy link
Contributor

mhegazy commented Mar 7, 2016

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 and undefined

JavaScript has two ways that developers use today to denote uninitialized or no-value. the two behave differently. where as null is completely left to user choice, there is no way of opting out of undefined , so:

function foo(a?: number) {
   a // => number | undefined
}

function bar() {
    if(a) return 0;
}

bar() // => number | undefined

a will always implicitly has undefined, and so will the return type of bar.

Nullability

Given the JS semantics outlined above, what does a nullable type T? mean:

  1. T | null | undefined
  2. T | undefined

1. T | null | undefined

It is rather subtle what ? means in different contexts:

function bar(a: number?, b?: number) {
  a // => number | undefined | null
  b // => number | undefined
}

2. T | undefined

This is more consistent, the ? always means to | undefined; no confusion here.

T?? can be used to mean T | undefined | null or T? | null

@mhegazy mhegazy added the Discussion Issues which may not have code impact label Mar 7, 2016
@basarat
Copy link
Contributor

basarat commented Mar 8, 2016

I thought that nullable Foo meant Foo | null | undefined i.e. both null and undefined are considered valid nulls.

Belief basis:

From #7140
image

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 🌹.

@RyanCavanaugh
Copy link
Member

Just to explore what this means

Option 1: T? = T | undefined

This is an argument for T? being exactly equal to T | undefined.

Consider some code:

declare function f(x?: number): void;
declare function g(x: number?): void;

Should f and g be identical in behavior when invoked with one argument ? Intuition seems to point to yes. If null were in the domain for x in g, then these functions would not be equivalent, which is potentially very confusing.

Now let's think of the implementation of g:

function g(x: number?) {
  if (g === undefined) {
    // do something
  } else {
    // Safe to invoke a method on `g` ?
    console.log(g.toFixed());
  }
}

Is this code correct? Intuition, at least among people who don't use null, says "yes".

Similarly, for types:

interface Thing {
  height?: number;
  weight: number?;
}
let x: Thing = ...;
let h = x.height;
let w = x.weight;

If null were in the domain of number?, then h could be undefined or number, but w could be undefined, number, or null. Again, all things equal, symmetry is very preferred here.

Pros

  • Preserves symmetry between (x: number?) => void and (x?: number) => void
  • Makes things very smooth for programs that don't use null
  • undefined is the only true "missing" value in JavaScript (modulo some obscure DOM APIs)

Cons

  • Makes things more awkward for programs that use null as a sentinel non-value

Option 2: T? = T | undefined | null

This is an argument for T? being exactly equal to T | undefined | null.

Consider some code:

function f(): number? {
  return Math.random() > 0.5 ? 32 : null;
}
const x = f();
Debug.assert(x !== undefined);

Is this code correct? It certainly looks like it. But if T? is T | undefined, then this program has two errors - f has an incorrect return null codepath, and x can't be compared with undefined.

Also consider this code:

interface Thing {
  name?: string;
  value: string?;
}
// OK
var x: Thing = { value: null };
// Also OK
var y: Thing = { name: undefined, value: null };
// Error
var z: Thing = { name: null, value: null };

Again, at inspection, this program looks good.

Pros

  • Makes it easy to write programs that use null

Cons

  • Complete lack of symmetry between (x: number?) => void and (x? number) => void

Option 1a: Include T?? = T | undefined | null

If T? = T | undefined, then we might want an alias for T | undefined | null; given available punctuation, ?? seems like the best choice (T~ and T* were mentioned but considered generally worse).

Pros

  • Convenient
  • Generally easy to understand
  • Much shorter than Nullable<T>

Cons

  • T?? looks somewhat silly, at least for now
  • T?? isn't the same as (T?)?
  • Maybe no one uses this and it's a waste of effort / complexity

@RyanCavanaugh
Copy link
Member

@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 null)

@mhegazy
Copy link
Contributor Author

mhegazy commented Mar 8, 2016

As @RyanCavanaugh, this issue is to illicit feedback to put in #7140; so #7140 should not be used as the expected behavior yet.

@basarat
Copy link
Contributor

basarat commented Mar 8, 2016

My vote: Allowing null to be a valid nullable aka T | null | undefined

Reasons:

@RyanCavanaugh looking back I did originally misread the question here 🌹 :)

@evmar
Copy link
Contributor

evmar commented Mar 8, 2016

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 foo.bar() on foo?"

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 null in the above, so I'm not sure how we can type getElementById unless it has return type HTMLElement? and that type must include null. From that perspective null and undefined should be treated the same.

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 foo(x?: number?) mean totally different things to me. The first one is a modifier on the function itself -- note that it modifies the arity of the function -- and the second is a statement about the type of x (e.g. it could just as well be replaced with a typedef). Perhaps it's just because I've looked at too much closure code, which uses = instead of ? for optional arguments though!

@evmar
Copy link
Contributor

evmar commented Mar 8, 2016

Another use case to consider:

let map: {[key: string]: MyObject?} = ...;
let x = map['foo'];
x.bar();

@spion
Copy link

spion commented Mar 8, 2016

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 (x: number?) => void and (x?: number) => void ? I interpret the two types very differently i.e. one is a function that takes a mandatory argument that may be null or undefined, the other is a function that can take an optional argument. They also read very clearly to me:

  • definite x, maybe number maybe null | undefined
  • maybe x, if provided, definitely number.

There is a 3rd signature: (x?: number?) which would be

  • maybe x, and if provided, maybe number, maybe not.

And finally we add the following quirk of JavaScript: passing undefined should be indistinguishable from missing values. * This is consistent with standard JS semantics when accessing arguments and object properties directly. Now we get:

  • x:number? - x is number or null or left out
  • x?:number - x is number or left out.

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 x?:number should be different from x:number?. My rule of thumb guess is that it shouldn't be, because most missing argument/field checks are super-sloppy i.e. if (!x), better ones are still sloppy if (x != null) (common enough to be now included as a special case exception in JavaScript standard style) and many developers learn to avoid using the name undefined, as it can be shadowed.

On the other hand, I imagine that old style typeof x === 'undefined' checks are still quite common. In my experience however, they were often considered bad precisely because they fail to account for nulls. Instead this alternative was normally recommended: x !== null && typeof x !== 'undefined' which we now consider unnecessarily verbose as its exactly the same as x != null

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...

@EmmanuelOga
Copy link

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.

@weswigham
Copy link
Member

I prefer number? as a shorthand for number | undefined rather than number | undefined | null because it composes better (you can add null, it's impossible to subtract it), and serves as a better indicator of how most builtins function. (I'd rather be sprinkling number?'s everywhere rather than number | undefined, that's for sure!). So many builtin structures in JS (object indexes, for one!) only ever introduce undefined and never null - having to write that out by hand each time seems comparably tedious.

In any case, having base types which include neither value lets me create more accurate typings, this is just a contest for shorthand syntax. T? and T?? look both short and easy to work with, so I'm all for them, but I'll easily write out type Nullable<T> = T | null; and type Undefinable<T> = T | undefined; in my own code, if need be. And I will treat them separately because I don't want to assign something nullable to something undefinable, since then it could get a null, which would be bad.

@RyanCavanaugh
Copy link
Member

TL;DR: Flow was right, T? should be T | null. Seeking symmetry around x?: number and x: number? is a trap.

Let me put forth a new option: Unifying around the idea that undefined is the missing value and null is the non-value, with the logical outcome being that T? is actually T | null and T?? is T | undefined | null

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 arguments.length, the implementation of fn cannot distinguish fn(undefined) from fn(), but we're saying the latter is an error and the former is OK. And at the same time, we're saying that fn(null) is wrong even though fn tried to say that it could accept a number?, which raises the question of what we even call this kind of type since "nullable" is clearly off the table.

Let's consider instead that undefined means "missing".

function fn(x: number?) { /* implementation here */ }
fn(undefined); // Error, x is missing
fn(null); // OK
fn(); // Error, x is missing

Now the cases where fn can distinguish what got passed to it actually correspond to what the compiler would consider valid. A 1:1 correspondence between runtime and compile-time behavior should be a good sign.

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 null back into the type system.

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 isPoint function, they would probably write:

function isPoint(a: Point) {
  // Check for 'x' property
  return a.x !== undefined;
}

Again, by saying T? is T | undefined, we've diverged the type system behavior from the runtime behavior as commonly seen in practice. Not good.

Separating concerns and T??
JavaScript code has two concerns to deal with:

  • Does this property exist (or for variables, is it initialized) ?
  • Does this property have a value ?

The first concern, existence or initialization, is checked by using === undefined. This tells if you if a property exists or a variable has been initialized. "Present according to hasOwnProperty, but has the value undefined" is not a state that the vast majority of JS code recognizes as being meaningful.

The second concern, having a value, is conditional on existence/initialization. Only things that exist and aren't null have a value.

It's a mistake to try to merge these concepts into one unified thing. See the section on undefined as sentinel for more comments on this.

For an arbitrary x, there are four states we can be in:

  1. x has a value (T)
  2. x might be missing, but if it's not missing, it has a value (T | undefined)
  3. x is not missing, but might not have a value (T | null)
  4. x might be missing, and if not missing, might not have a value (T | undefined | null)

Given this:

  • Clearly T is state 1 (T)
  • Clearly T?? is state 4 (T | undefined | null)
  • Unclear: Is T? state 2 (T | undefined) or state 3 (`T | null)?

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:

  • alpha corresponds to state 2, T | undefined. When x is not missing, it has a value.
  • beta corresponds to state 3, T | null. x is not missing, but may not have a value.

In other words, x: number? is number | null. x?: number is number | undefined.

undefined as sentinel and the pit of success
Our informal twitter poll showed ~80% of people using null in some regard. I think this makes a lot of sense and we shouldn't steer people away from that pattern based on our bias of how the compiler happens to be implemented.

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 null instead, and vice versa. For example, unknownType and unknownSymbol are, in the vast majority of cases, simply checked for reference identity the same way we would for null if that keyword were allowed in our codebase. Indeed, failing to check for unknownSymbol is a common source of bugs that we could avoid if we had a null-checking type system and used the null symbol to mean unknown (thus ensuring that all necessary code guarded against it).

Conversely, we use undefined as a sentinel in dangerous ways. An informative thing to do is look at where in our codebase we write expr === undefined where expr is a bare identifier (not a property access expression, where we're usually checking for an uninitialized (i.e. missing!) field). In nearly every case, we're using undefined as a sentinel value, in which case the choice of null / undefined / reference sentinel is immaterial, and arguably poorly chosen.

For example, this code uses undefined as a sentinel (comment is as written):

    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 getEffectiveArgumentCount and getEffectiveArgument agree in implementation? They're implemented over 200 lines apart. If we go out-of-bounds at return args[argIndex];, the manifestation is going to be very, very subtle. We could have allocated a Expression syntheticArgument sentinel and checked for that instead, turning out-of-bounds errors into immediate crashes. Alternatively, we could have used null to indicate that the argument is synthetic. Using undefined here seems simply dogmatic -- two safer values were available, but not used. The fact that we generally get away with this sort of thing shouldn't be seen as a positive case for using undefined as a universal sentinel.

@jesseschalken
Copy link
Contributor

@RyanCavanaugh That makes a great deal of sense. 👍 (null for things that are nullable. undefined for things that may not exist.)

Also, from a purely DX perspective, if null and undefined are conflated (eg bar: string? === bar?: string, or the former overlaps the latter), it is easy to accidentally omit properties or arguments just because they happen to be nullable. Eg:

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
    })
}

TL;DR: Flow was right, T? should be T | null. Seeking symmetry around x?: number and x: number? is a trap.

The page @basarat linked to (http://flowtype.org/docs/nullable-types.html) says Flow considers ?T effectively as T | null | undefined. Is that right?

@dallonf
Copy link

dallonf commented Mar 8, 2016

@RyanCavanaugh This proposal makes sense to me. Feels like the best we can do in a language that supports both null and undefined as distinct but absent values...

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
}
  1. y and z are the same type and behave the same way
  2. {w: null} is assignable to Foo, omitting both x and y which use the existing "optional" property syntax, and z which uses the new "super-nullable" (for lack of a better name) syntax
  3. There is a distinction between optional (or "undefinable") properties like x?: string and nullable properties like x: string?.
  4. {}is not assignable to Foo, it's missing w. Similarly, neither is {x: "bar"}.

@zpdDG4gta8XKpMCd
Copy link

is this so important to come up with the consistent syntax? let's face it there are 3 cases to address:

  • T | null
  • T | undefined
  • T | null | undefined

and then what about this:

  • null | undefined

can we just keep it the way it is:

  • to keep it consistent with the current syntax and semantics, let's leave optional values value?: T the way the are T | null | undefined
  • for everything else let's not make up any more syntax and use the union types or aliases: var value: string | null | undefined; or type Nullable<T> = T | null;

if you ask me the var value: T | null | undefined is as clear as a day, whereas var value? : T?? is what I type when I am hungover

the argument that T | null | undefined is longer to type is faint whoever wants can make it as short as

type u = undefined;
type n = null;
var value: T | u | n; // <-- fixed!

// or

type un = undefined | null;
var value: T | un;

@weswigham
Copy link
Member

If you want another example of prior work other than Flow, in JSDoc
comments for the closure compiler, T? actually equates to T|null while
T= is T|undefined.

On Tue, Mar 8, 2016, 9:57 AM Aleksey Bykov notifications@github.com wrote:

if you ask me the var value: T | null | undefined is as clear as a day,
whereas var value? : T?? is what I type when I am hungover

the argument that T | null | undefined is longer to type is faint whoever
wants can make it as sort as

type u = undefined;
type n = null;
var value: T | n | u; // <-- fixed!


Reply to this email directly or view it on GitHub
#7426 (comment)
.

@tinganho
Copy link
Contributor

tinganho commented Mar 8, 2016

I prefer solution 1.

Though, I wonder if anyone have considered separating the meaning of ? for property names and type names?

  1. ? for property/variable/param names means T | undefined for whatever annotated type.
  2. ? for type names means T | null.

Lets begin with explaining the latter 2. first. I think every JS function that are meant to return type T should return a value that corresponds to type T or null. This is consistent with other browser API:s such as document.getElementById('none'); // null. This is also consistent with other programming languages.

For 1.. There is already a way of defining optional params and properties in Typescript. And it means T | undefined right now.

undefined means uninitialized and null means novalue. Checkout this SO thread http://stackoverflow.com/questions/5076944/what-is-the-difference-between-null-and-undefined-in-javascript. Many programmers that come from other programming languages have hard time to separate the meaning of null and undefined. Though I believe that pure JS developers don't have this problem.

C++ also default initializes to undefined behaviour and not null. Javascript is just a copy of that.

Also quoting one of the comments of the first answer in the SO thread:

You may wonder why the typeof operator returns 'object' for a value that is null. This was actually an error in the original JavaScript implementation that was then copied in ECMAScript. Today, it is rationalized that null is considered a placeholder for an object, even though, technically, it is a primitive value.

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 T | null | undefined as:

let s?: string?;// string | null | undefined

One other downside is, there might be a few people who have written function that returns T | undefined. In my view this is already broken in the first place. Though I think it is extremely rare. One could provide an escape hatch to define the correct type in those cases:

function getString(): string | undefined {
    if (Date.now() % 2 > 0) {
        return 'hello world';
    }
    return undefined;
}

Pros

  • consistent with the current JS and TS semantics.

Cons

  • Breaks existing code.

@zpdDG4gta8XKpMCd
Copy link

there is one more problem with the current semantics of value?: T, namely:

  • it doesn't require being initialized

and it's a huge PITA, because turns out it's not the same as value: T | null | undefined that has to be initialized, is it?

@ahejlsberg
Copy link
Member

A few assertions to ground this discussion:

  • The semantics of T | null, T | undefined, and T | null | undefined are not in question and we're not proposing any changes from what is described in the introduction to Non-nullable types #7140.
  • An optional parameter or property declaration automatically adds undefined (but not null) to the type of the parameter or property (i.e. x?: T is the same as x?: T | undefined) and we're not proposing changes to this.

The purpose of this discussion is only to debate which of the following shorthand notations we want:

  1. T? means T | null | undefined.
  2. T? means T | undefined.
  3. T? means T | undefined and T?? means T | null | undefined.

In other words, we're bikeshedding on the meaning of the ? type modifier and nothing else.

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;

@mhegazy
Copy link
Contributor Author

mhegazy commented Mar 8, 2016

To summarize @RyanCavanaugh proposal in the same terms:

4. T? means T | null.
5. T? means T | null and T?? means T | null | undefined.

@zpdDG4gta8XKpMCd
Copy link

you forgot the 3rd case: when I need T | undefined I go... T | undefined correct?

@mhegazy
Copy link
Contributor Author

mhegazy commented Mar 8, 2016

@Aleksey-Bykov as @ahejlsberg mentioned, T | undefined and T | null are always valid. you can use them whenever you need or like. the question is what the shorthand notation T? and possibly T?? would mean. null and undefined are now valid type names.

@ahejlsberg
Copy link
Member

Ok, so we now have five options:

  1. T? means T | null | undefined.
  2. T? means T | undefined.
  3. T? means T | undefined and T?? means T | null | undefined.
  4. T? means T | null.
  5. T? means T | null and T?? means T | null | undefined.

@zpdDG4gta8XKpMCd
Copy link

you cannot put 3 pigeons in 1 cage, i vote for 1: T? means T | null | undefined at least it's aligned with the current semantics of optional parameters/properties

although can't see what sort of a well supported argument can be made here

@weswigham
Copy link
Member

Can we add option 6, have a modifier for both?:

T? means T | null, T= means T | undefined, and T?= or T=? means T | null | undefined. This would mirror the closure compiler's syntax for nullable/optional.

@spion
Copy link

spion commented Mar 8, 2016

Do we really need T?? though? Assuming we choose something like option 5, I wager that T?? will actually be very rare. In function arguments you can use:

(x?: T?)

instead, and in record fields you can use

{x?: T?}

instead.

Which AFAICT means the only place where a T?? annotation can appear is for un-inferred local variables.

For that case, could we not use the following syntax?

var x?:number?; 

Reads: Local variable x may or may not "exist", and if it does, its either of type T or null.

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?

@weswigham
Copy link
Member

@spion relying on ?'s on initializers for one of the unions would mean there's no shorthand for casting to that form.

@zpdDG4gta8XKpMCd
Copy link

@spion x?: number? syntax doesn't work as well for standalone types

@spion
Copy link

spion commented Mar 8, 2016

@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.

dokidokivisual added a commit to karen-irc/karen that referenced this issue May 4, 2016
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 -->
@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 24, 2016

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 null.
Furthermore, for everyone advocating for the use of explicit null for indicating an optional value, it is worth considering that, just as most languages do not have both null and undefined, most languages do not define both an Option<T> and a Maybe<T> type in their standard library. Haskell uses Maybe, Scala uses Option, but they are both expressing the same thing.

@jods4
Copy link

jods4 commented Jun 26, 2016

@aluanhaddad

That is definitely a reason not to use null.

Definitely? That's radical and it seems many people use null nonetheless. Yes this is unfortunate. Covariant arrays in C# are unfortunate as well. As much as I would like to see both of those reversed, they are not enough to stop me using null in JS or arrays in C#.

BTW you will not be able to completely evade null, as some APIs (even built-in ones) return it.

Haskell and Scala (and most languages) don't have both a null and undefined equivalents because AFAIK they are not dynamic languages.
undefined exists to represent the fact that (a) x.frob may not even exist, which is different from (b) existing but being empty aka null.
Since you want to compare with static languages, in Java (a) is a compilation error and (b) is null. So yes, both exist, in a way.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 26, 2016

@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 null instead of throwing an error when an attempt is made to retrieve of value for a non-existent key. Furthermore, JavaScript uses undefined in almost all the places where most languages would use null. It has nothing to do with Dynamic typing at all, it is actually about the semantics of the "no such member" case.

@basarat
Copy link
Contributor

basarat commented Jun 26, 2016

JavaScript object is a hashtable that returns null instead of throwing an error

*typo (I think). You meant to say undefined. I agree with you. Crockford agrees with you. Its undefined all the way. But I just have null inconsistently because of dealing with dom / regex / nodejs :) So I end up with both :-/. But never check explicitly against one ... just == null or == undefined (either works for both) 🌹

@zpdDG4gta8XKpMCd
Copy link

the problem with both undefined and null is that in practice there might be
and often is more than 2 flavors of a value absence that all need to be
considered at the same time:

  • unspecified default parameter (undefined?)
  • or specified but not found (null?)
  • or specified as not applicable (now what?)
  • or specified as not yet decided (um, help)

and worse comes to worst next time the same values camean completely
different things

now pardon me, what exactly are we discussing here?
On Jun 26, 2016 19:48, "Basarat Ali Syed" notifications@github.com wrote:

JavaScript object is a hashtable that returns null instead of throwing an
error

*typo (I think). You meant to say undefined. I agree with you. Crockford
agrees with you. Its undefined all the way. But I just have null
inconsistently because of dealing with dom / regex / nodejs :) So I end
up with both :-/. But never check explicitly against one ... just == null
or == undefined (either works for both) 🌹


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#7426 (comment),
or mute the thread
https://github.com/notifications/unsubscribe/AA5Pzd4Itjc6Dv7ack2i5m5cswfbyNdcks5qPw-1gaJpZM4HrL9y
.

@jesseschalken
Copy link
Contributor

jesseschalken commented Jun 27, 2016

static equivalent of a JavaScript object is a hashtable that returns null instead of throwing an error when an attempt is made to retrieve of value for a non-existent key

Consider if in a statically typed language you have a Map<Maybe<string>> with a method Map<T>.get(key:string):Maybe<T>. Then the result of calling .get() would be a Maybe<Maybe<string>> and you would deal with the result with something like:

match (map.get('key')) {
    Nothing => echo "key doesn't exist"
    Just x => match (x) {
        Nothing => echo "NULL was stored in the map (meaning depends on the purpose of the map)"
        Just s => echo "Got a value: {s}"
    }
}

If Maybe isn't an algebraic data type but is actually a union with a special null value, then the Maybe<Maybe<string>> becomes string|null|null, and the two different nulls become indistinguishable:

let val = map.get('key')
if (val === null) {
    echo "key might not have existed, or it might have existed but been set to null, don't know :("
} else {
    // ....
}

This is why undefined exists. As soon as you (as a language designer) go down the path of the presence/absence of a property being indicated in the result of fetching it, undefined becomes necessary to distinguish that case from a null that the developer has purposely placed there. null is for developer use for things that are nullable. undefined is for language use as the result for fetching things which don't even exist or haven't even been set. See here for an example of why the difference is important.

When it comes to typeof null === 'object', the reason for that is that JavaScript was originally intended to be "like Java" and in Java (and C/C++ for that matter) the only place you can find a null is somewhere you might otherwise find an object/pointer. null was originally intended to mirror a null reference or null pointer from other languages, and so null was really a special instance of the reference/pointer/"object" type. (In practice of course JavaScript is a dynamic language and null stands by itself and can be used for whatever purpose you wish.)

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 29, 2016

@> *typo (I think). You meant to say undefined.

@basarat I actually did mean null because I was trying to map the concept of a JavaScript object in terms of a some arbitrary, statically typed language to point out that a JavaScript object would be modeled as an associative collection type. But thank you ❤️

My point was exactly what you went on to say

But I just have null inconsistently because of dealing with dom / regex / nodejs :) So I end up with both :-/. But never check explicitly against one ... just == null or == undefined (either works for both)

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.

When it comes to typeof null === 'object', the reason for that is that JavaScript was originally intended to be "like Java" and in Java (and C/C++ for that matter) the only place you can find a null is somewhere you might otherwise find an object/pointer. null was originally intended to mirror a null reference or null pointer from other languages, and so null was really a special instance of the reference/pointer/"object" type.

That is wrong. The fact that typeof null === 'object was a bug that cannot be fixed. It was not intended to be this way. How is an undefined reference different from a null reference?

@jesseschalken
Copy link
Contributor

jesseschalken commented Jun 29, 2016

That is wrong. The fact that typeof null === 'object was a bug that cannot be fixed. It was not intended to be this way. How is an undefined reference different from a null reference?

See

Also note the distinctly different wording for undefined and null in the ES spec (emphasis mine):

4.3.10 undefined value

primitive value used when a variable has not been assigned a value

4.3.12 null value

primitive value that represents the intentional absence of any object value

The original tagged union for a JavaScript value had undefined as a distinct tag and an "object" tag with a pointer to an object. null was an instance of the "object" tag with a null pointer. null served the purpose of mirroring the null reference from Java, while undefined served the purpose as the value of things which don't exist/haven't been set.

This way you could, say, take a Java object and ask if a property exists with === undefined, and ask whether a property whose type is an object if it contains null with === null. Those are two different operations.

typeof simply queried the type tag, so typeof on a Java property of type object would always return "object", regardless of whether the reference inside happened to be null.

@aluanhaddad
Copy link
Contributor

@jesseschalken I know the behaviour is well specified and standardized, I'm saying it's semantically wrong for null to have type of object.
See the remarks by Brendan Eich in this thread
http://wiki.ecmascript.org/doku.php?id=discussion:typeof
Actually found my way there by following some of the links you provided which were quite interesting.

@xogeny
Copy link

xogeny commented Nov 7, 2016

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:

interface Foo {
  name?: string;
}

Then the type of name is string | undefined. Is this correct? I ask because I can then write:

  let foo: Foo = {}; // Ok

However, if I do this:

interface Bar {
  name: string | undefined; // Same type, no?
}

The I get this:

  let bar: Bar = {}; // Error: missing field name

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:

interface Foo<T> {
  name: string | T;
}
type PartialFoo = Foo<undefined>;
type CompleteFoo = Foo<never>;

I dug through several threads, but I didn't see any reason why name: string | undefined should have different semantics than name?: string.

Thanks.

@jods4
Copy link

jods4 commented Nov 7, 2016

@xogeny
There is a slight difference and it applies equally to optional parameters and optional members.
The ? notation is not just a shortcut for T | undefined. It actually means that the parameter/member is optional and this is a distinct concept.

If you declare

function x(n?: number) {}

Then the parameters n is optional and you can call the function with x().
Of course in that case, n would be undefined in the function body, which is why its type definition is extended to number | undefined. But this rather a side-effect of optionality.
Writing function x(n?: number | undefined) is legal and would be strictly equivalent.

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: x(undefined).
Calling x() is not legal in that case.

The optionality part can be seen more clearly if you try to add more parameters. The following declaration is illegal: function x(a?: number, b: number) because all parameters after the first optional parameter have to be optional as well.

It works the same for optional members.
I agree with you that { x?: number } and { x: number | undefined } are mostly equivalent, but strictly speaking, there are differences. Consider the following examples:

'x' in { x: undefined } === true;
'x' in { } === false;

Object.assign({x: 3}, {x: undefined}) // == {x: undefined }
Object.assign({x: 3}, { }) // == {x: 3}

@ghost
Copy link

ghost commented Nov 16, 2016

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 void type. For example, the following which is legal TypeScript confirms the void type is inhabited by the value undefined.

const X: void = undefined;

This makes me wonder if the type undefined is simply a literal value type in exactly the same vein that it is legal to write:

const X: true = true;
const Y: undefined = undefined;

Accordingly could this discussion be usefully framed around the concepts of nullability versus voidability?

And then consider writing:

type Nullable<T> = T | null;
type Voidable<T> = T | void;

const myDeliberatelyNoValuedStringValue: Nullable<string> = null;
const myMissingStringValue: Voidable<string> = undefined;

const myPossiblyNoValuedStringValue: Nullable<string> = "foobar";
const myPossiblyMissingStringValue: Voidable<string> = "foobar";

@mhegazy
Copy link
Contributor Author

mhegazy commented Nov 16, 2016

void only makes sense in the return type position of a function to denote the "absence of a value". Before TS 2.0, ppl have used void to mean undefined for values, which is close, but not correct. So for your example, Voidable<T> should be really T | undefined.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests