Skip to content

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

Description

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

Activity

  1. basarat commented on Mar 8, 2016

    @basarat
    Contributor

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

  2. RyanCavanaugh commented on Mar 8, 2016

    @RyanCavanaugh
    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
  3. RyanCavanaugh commented on Mar 8, 2016

    @RyanCavanaugh
    Member

    Basarat Ali Syed (@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)

  4. mhegazy commented on Mar 8, 2016

    @mhegazy
    ContributorAuthor

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

  5. basarat commented on Mar 8, 2016

    @basarat
    Contributor

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

    Reasons:

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

  6. evmar commented on Mar 8, 2016

    @evmar
    Contributor

    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!

  7. evmar commented on Mar 8, 2016

    @evmar
    Contributor

    Another use case to consider:

    let map: {[key: string]: MyObject?} = ...;
    let x = map['foo'];
    x.bar();
  8. spion commented on Mar 8, 2016

    @spion

    I vote for option 1 as described by Mohamed Hegazy (@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...

  9. EmmanuelOga commented on Mar 8, 2016

    @EmmanuelOga

    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.

  10. weswigham commented on Mar 8, 2016

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

  11. RyanCavanaugh commented on Mar 8, 2016

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

  12. jesseschalken commented on Mar 8, 2016

    @jesseschalken
    Contributor

    Ryan Cavanaugh (@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 Ali Syed (@basarat) linked to (http://flowtype.org/docs/nullable-types.html) says Flow considers ?T effectively as T | null | undefined. Is that right?

  13. dallonf commented on Mar 8, 2016

    @dallonf

    Ryan Cavanaugh (@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"}.
  14. zpdDG4gta8XKpMCd commented on Mar 8, 2016

    @zpdDG4gta8XKpMCd

    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;
    
  15. 136 remaining items

  16. added a commit that references this issue on May 4, 2016
  17. aluanhaddad commented on Jun 24, 2016

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

  18. jods4 commented on Jun 26, 2016

    @jods4

    Aluan Haddad (@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.

  19. aluanhaddad commented on Jun 26, 2016

    @aluanhaddad
    Contributor

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

  20. basarat commented on Jun 26, 2016

    @basarat
    Contributor

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

  21. zpdDG4gta8XKpMCd commented on Jun 27, 2016

    @zpdDG4gta8XKpMCd

    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
    .

  22. jesseschalken commented on Jun 27, 2016

    @jesseschalken
    Contributor

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

  23. aluanhaddad commented on Jun 29, 2016

    @aluanhaddad
    Contributor

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

    Basarat Ali Syed (@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?

  24. jesseschalken commented on Jun 29, 2016

    @jesseschalken
    Contributor

    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.

  25. aluanhaddad commented on Jul 1, 2016

    @aluanhaddad
    Contributor

    Jesse Schalken (@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.

  26. xogeny commented on Nov 7, 2016

    @xogeny

    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.

  27. jods4 commented on Nov 7, 2016

    @jods4

    Michael Tiller (@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}
  28. mhegazy commented on Nov 16, 2016

    @mhegazy
    ContributorAuthor

    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.

  29. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DiscussionIssues which may not have code impact

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions