TypeScript: [Request for feedback] Nullable types, `null` and `undefined`
With the work on Nullable types in https://github.com/Microsoft/TypeScript/pull/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:
- T | null | undefined
- 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
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 5
- Comments: 162 (81 by maintainers)
TL;DR: Flow was right,
T?should beT | null. Seeking symmetry aroundx?: numberandx: number?is a trap.Let me put forth a new option: Unifying around the idea that
undefinedis the missing value andnullis the non-value, with the logical outcome being thatT?is actuallyT | nullandT??isT | undefined | nullMotivating example 1: Functions
This is very much wrong. Outside of dodgy stuff like checking
arguments.length, the implementation offncannot distinguishfn(undefined)fromfn(), but we’re saying the latter is an error and the former is OK. And at the same time, we’re saying thatfn(null)is wrong even thoughfntried to say that it could accept anumber?, 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
undefinedmeans “missing”.Now the cases where
fncan 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:
This behavior is the same as Option 1 above, except that we don’t need to think about how to introduce
nullback into the type system.Combining the two:
Motivating Example 2: Objects
Should this be legal? If we asked a JS dev to code up an
isPointfunction, they would probably write:Again, by saying
T?isT | 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: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 tohasOwnProperty, but has the valueundefined” 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
nullhave a value.It’s a mistake to try to merge these concepts into one unified thing. See the section on
undefinedas sentinel for more comments on this.For an arbitrary
x, there are four states we can be in:xhas a value (T)xmight be missing, but if it’s not missing, it has a value (T | undefined)xis not missing, but might not have a value (T | null)xmight be missing, and if not missing, might not have a value (T | undefined | null)Given this:
Tis state 1 (T)T??is state 4 (T | undefined | null)T?state 2 (T | undefined) or state 3 (`T | null)?We can answer this by writing two declarations:
If we believe in separation of concerns, as I think we should, then:
alphacorresponds to state 2,T | undefined. Whenxis not missing, it has a value.betacorresponds to state 3,T | null.xis not missing, but may not have a value.In other words,
x: number?isnumber | null.x?: numberisnumber | undefined.undefinedas sentinel and the pit of success Our informal twitter poll showed ~80% of people usingnullin 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
nullinstead, and vice versa. For example,unknownTypeandunknownSymbolare, in the vast majority of cases, simply checked for reference identity the same way we would fornullif that keyword were allowed in our codebase. Indeed, failing to check forunknownSymbolis a common source of bugs that we could avoid if we had anull-checking type system and used thenullsymbol to mean unknown (thus ensuring that all necessary code guarded against it).Conversely, we use
undefinedas a sentinel in dangerous ways. An informative thing to do is look at where in our codebase we writeexpr === undefinedwhereexpris 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 usingundefinedas a sentinel value, in which case the choice ofnull/undefined/ reference sentinel is immaterial, and arguably poorly chosen.For example, this code uses
undefinedas a sentinel (comment is as written):The implementation is:
Where’s the bounds checking in that code? It’s in the calling code:
How do we know that
getEffectiveArgumentCountandgetEffectiveArgumentagree in implementation? They’re implemented over 200 lines apart. If we go out-of-bounds atreturn args[argIndex];, the manifestation is going to be very, very subtle. We could have allocated aExpressionsyntheticArgumentsentinel and checked for that instead, turning out-of-bounds errors into immediate crashes. Alternatively, we could have usednullto indicate that the argument is synthetic. Usingundefinedhere 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 usingundefinedas a universal sentinel.Using
undefinedfor nullability is emphatically wrong. Code that does so is either broken or is waiting to be broken.I think that by not introducing
x: T?forx: T | null, but preservingx?: Tforx: T | undefined, TypeScript is actually encouraging the brokenness, because one has a shorthand and the other does not. Of course the one that has the shorthand is going to be used.TL;DR:
T? = T | nullis the right way to go.This is going to be a though sell because you have chosen to explicitly forbid
nullfrom your codebase, so you have bias. Please think about this issue with an open mind and ask yourself not what would suit your code today, but what is the most logical, coherent way to design future projects.I would also stress out that this is not bikeshedding, it’s important. Yes the way the type system work is mostly worked out, and
T?is “just” syntactical sugar. But as a built-in language facility, it’s going to become prescriptive of what an empty value is. New programmers coming to TS will learn thatT?is possibly missingTand they will use just that. None of them are going to say: “oh that’s so a bad choice, I’m going to usetype Opt<T> = T | null | undefined”. It is nice that you can do that, but the reality is that most programmers won’t. And even ifOpt<T>is ok, it’s nowhere as convenient as?for stuff that is going to be used a lot and sometimes composed (tryOpt<Opt<number>[]>).I was happy to see @RyanCavanaugh’s long comment above because I agree 100% with him. 👍
He explains clearly that in JS
undefinedrepresents non-existent values: properties that are not defined, variables that are not initialized, missing keys in aMap.get(), missing items in anArray.find(), optional parameters, out of bounds array accesses and more. This is not the same thing asnullwhich is a defined value, that denotes the “empty”, “none” concepts.I believe that the shortcut
T?should denote what we want people to commonly use to indicate an empty value. And at the core of javascript, a known empty value isnull.Using
undefinedas your empty value is wrong: you can’t make the difference between a missing optional parameter and a passed empty value.You can’t make the difference between a key that is not present in a
Mapor a stored empty value:JS is a dynamic language, if you assume
dog: Dog?property on objects, havingnullmeans: “I don’t have a dog”. If your objects does not have adogproperty, you get theundefinedvalue, which means “I don’t know if this person actually has a dog or not, the information is missing”.As @RyanCavanaugh pointed out, “empty values” are an often required concept. So if you forbid
nullyou end up needing sentinel values… which actually are exactly asnullbut without the awesome new TS null safety checks.For performance, you should define your object members and use
null. Modern JS engine optimize for monomorphism. When you declare a class, you should set all its properties in its constructor and never add or remove one afterwards. Leaving everythingundefinedmeans that each time you set one property your class shape changes. Even worse: if you don’t set them in the same order in every code path, you end up with different shapes! This hurts the JIT which generates less performant code.Preserving the difference between
undefinedandnullis also interesting when debugging. If my program crashes onx.hello()andxisundefined, I know that I have a different kind of problem vs ifxisnull. In the former it’s probably because I used a codepath wherexwas not initialized at all. In the latter it’s that my code was not expecting an empty value. And the interesting point here is that with TS 2.0 you should never getnullerrors ever. On the other handundefinedare unfortunately always possible because of JS nature (e.g. an out-of-bound array access).If you write your programs this way, I don’t even believe that
T??is something that we need. You would declare nullable types asT?meaningT | null. Optional parameters are implicitely undefined:f(x?: T)is in factT | undefined. Optional members are implicitely undefined as well:{ x?: T }is in factT | undefined. In itself,undefined(as a type) would almost never been used verbatim because it pops up mostly from the JS runtime. The only place where I would still seeundefinedused is as the return type for functions that want to say “no result”, for example:Array<T>.find((T, number) => boolean): T | undefinedAnd to be honest in those very rare cases I would prefer typingT | undefinedrather than the crypticT??. Incidentally that also impliesT?? = T | undefinedwhich is probably not what you’d expect.I could also live with proposal 7… it’s the non prescriptive, everybody-is-happy middle ground. The drawback is that this is cryptic like hell.
@jesseschalken We can always add
T?later, but we can’t take awayx?: T. I should add that we’re also planning to introduce two type aliases inlib.d.ts:@xogeny There is a slight difference and it applies equally to optional parameters and optional members. The
?notation is not just a shortcut forT | undefined. It actually means that the parameter/member is optional and this is a distinct concept.If you declare
Then the parameters
nis optional and you can call the function withx(). Of course in that case,nwould beundefinedin the function body, which is why its type definition is extended tonumber | undefined. But this rather a side-effect of optionality. Writingfunction x(n?: number | undefined)is legal and would be strictly equivalent.On the other hand, if you declare
Then the parameter is not optional and you have to pass it, even when undefined, like so:
x(undefined). Callingx()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:I started a poll on
T?https://twitter.com/basarat/status/709515537580105728 (still in progress but I don’t need to wait for it to end to see the pattern). The results are very similar to the non syntax poll by the TypeScript team https://twitter.com/typescriptlang/status/707007550659493888My analysis:
nullandundefined. Some people try to use explicitnullonly, others try to useundefinedonly.The only way a
T?syntax can accommodate all these people is ifT?meansT | null | undefined. Thenull/undefinedare generally typeguarded with!=nullor!=undefinedanyways so the following should just work:For people that do care about the strong consistency of
nullorundefinedthe interfacesNullable/Optionalcan be used.I don’t think there will ever be a consensus for
T?to mean onlynullor onlyundefined.This is all purely my opinion 🌹
My vote: Allowing
nullto be a valid nullable akaT | null | undefinedReasons:
nullis used quite a lot in node forerrorargument to callbacks.nullis in the name of nullablenullandundefinedthe same : http://flowtype.org/docs/nullable-types.html@RyanCavanaugh looking back I did originally misread the question here 🌹 😃
both null and undefined can only encode a single bit of information , there is nothing that make one bit of information more expressive than another bit of information hence no distinction, am I clear so far?
now have 2 indistinguishable options and we need to make a hard choice, JavaScript to the rescue, undefined is already used by JavaScript for representing missing values, JavaScript doesn’t use null for anything saving it for humans, humans are free to use it anyway the want
here comes the point, undefined is better for missing values because the choice was already made in its favor, let’s just stick with it and let null go if we have to make this choice and define what T? is
I personally think that T? idea should be given more time, the optional feature should be shipped backed by unions and aliases
@aleksey-bykov Please be constructive and take a few moments to think about what kind of arguments other people might find convincing, rather than just conveying your own emotions about the issue. Just coming in here and saying something is “embarrassing” or “nonsense” or “a shame” isn’t going to change anyone’s mind, and isn’t useful evidence. The proposal of “Don’t add
T?” has been recognized and is something we will be giving due consideration to.As a practical matter, if we shipped this feature without
T?meaning anything, we’re going to get six issues a day from people suggesting both “HaveT?be a shortcut forT | undefined” and “HaveT?be a shortcut forT | null”. I guarantee it. There’s also strong precedent here asT[]is already sugar forArray<T>(which no one seems to complain about).?T?this is just embarrassing… seriously? people are going to be pointing at me and saying: look here comes the?T?guy, what a shameseriously let’s stop this question-sign-here-and-there nonsense!
why?
has any one asked themselves a question: why we need this new syntax when there are unions here available already right now
I vote for 4 or 5,
T? = T | null, because I agree that nullability and optionality are separate, orthogonal concepts, corresponding to valuesnullandundefinedrespectively, and TypeScript already has a shorthand for optionality,x?: T. The only thing that is missing is a shorthand for nullability,x: T?. If you want both, you can get both withx?: T?(orx: T??for option 5).I see no reason to conflate the two concepts, and I especially wouldn’t want parameters and properties to be unintentionally optional (by including
undefiend) because they were made nullable withT?instead ofT | null.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?
Unfortunately, it appears that x is
nullin the above, so I’m not sure how we can typegetElementByIdunless it has return typeHTMLElement?and that type must includenull. 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!*typo (I think). You meant to say
undefined. I agree with you. Crockford agrees with you. Its undefined all the way. But I just havenullinconsistently because of dealing withdom/regex/ nodejs 😃 So I end up with both 😕. But never check explicitly against one … just== nullor== undefined(either works for both) 🌹Not really. My question was actually asking: Is it wise to take vastly differing actions based on whether the outcome is
nullvs.undefined, considering there is no consistency in their provenance.No, I disagree with your interpretation of the JS spec. The arguments that you have mounted in favour of using
nullnow appear to have degenerated to “because it’s in the spec”. I’m saying the spec is neutral on this.@NoelAbrahams Do I understand you correctly? Basically, you’re saying that you’re not convinced by using
nullthe way ECMAScript says you should (even in the most recent specs); and that TS should deviate from the JS specs by promoting your view?You asked me where
undefinedvsnullwould actually make any difference, as if the points I made previously where purely rhetorical. I showed you a common abstraction as an example.If you use
nullas your intentional empty value then the memoization code above works perfectly. If you chooseundefinedyou’ll have to add a few workarounds to have a generic, working memoization. It’s not impossible mind you. It’s just friction because the JS libraries (hereMap) useundefinedwith a defined meaning that you do not want to adhere to.I respect your choice and I don’t want you to change your programming style. But I think you need a stronger argument for TS to encourage your style vs the official documentation of JS that can be read over the internet. Here’s the first StackOverflow result on the subject. I don’t think there’s a precedent for TS not following JS specs.
BTW you won’t avoid
nullcompletely as some API returns it, for instancedocument.getElementById()returnsnullif there is no matching element in the DOM.And honestly: there are no “nuances”. It’s easy if you take 2 minutes to read the one-liner description of each (and you end up using
null99% of the time). If you think I’m wrong, please post an example where the concept is confusing. There are a few API where the return type might not be obvious – e.g. shouldgetElementByIdhave returnedundefined? No library is perfect, but thankfully with static typing this isn’t an issue.Do you have any insight here? I am in no position to comment about that. Anyway we have to live with our mistakes. TS needs to embrace JS as it is defined today. BTW the fact that it’s a mistake doesn’t seem to make consensus judging by the polls and discussions.
Yes, and this reason is “by definition of
undefined: the key has not been assigned a value”. It proves that modern APIs still follow the old semantics. If ES committee wanted to design modern API so thatnullwas not used anymore, they would have modelled it like .NET did withDictionary: throw an exception if the key is not found or return a tuple (theoutparam inTryGet). They certainly would not have created an API that does not distinguish between “no key” and “empty value”. That is just assuming bad API design.@jods4,
If I understand you correctly, you would like to use
nullso that one can write code like your memoisation helper.That is precisely what I am arguing against. I do not advocate writing code based on the nuances of
nullandundefined.As to your assertion that somehow
nullis a carefully planned ECMASCript standard, I do not hold that view. My view is that it is a historical mistake. If we could start again there would be only one concept for missing/empty values. Writing code should be about reducing complexity - not enhancing it by introducing highly nuanced concepts.The fact that
map.getreturnsundefineddoes not prove anything in your favour. It does so for the same reason that indexing into an object literal or an array returnsundefined.Oh wow, I missed the whole “assigned-before-use checking” thing. +1, disregard everything I said in the last couple days. 😄
There are lots of things in the official JS standard that are there for historical reason. No one is going to argue that automatic semicolon insertion is a good thing, but that’s part of the standard too.
Regarding
Optional, just a note that Swift usesOptional<T>for the algebraic option type (i.e.Nothing | Just t), and many other languages (Rust, Scala etc) useOption<T>to mean that too. Might be a bit misleading to call it that (Though I suppose in the context of JS it makes sense)Current Status
Thanks everyone for the comments so far; it’s been very instructive. Dan posted some notes in #7488. Here’s where are after the design meeting yesterday.
In the absence of a strong consensus (possible understatement of the year), we felt it was better to not have
T?as a type syntax sugar for now. Everything else about this feature is relatively clear and it would be problematic to have a new syntax with unclear meaning be confusing users when it first appears.Additionally, there was a relatively good consensus that making definition file authors actually think (e.g. read the docs) about whether a particular function might return
nullorundefinedis a very good thing, rather than lots of people assuming thatT?is the right shortcut for whatever the function/property might do.Existing described behavior around optional properties and optional parameters remain unchanged, changing the implicit type of
x?: stringin those positions tostring|undefined. Additionally, the “possibly-uninitialized” variable syntax (let x?: string) will be supported, also implyingT | undefined.Going forward we’ll be looking at what nullability-compliant definition files on DefinitelyTyped look like. If there’s significant preponderance of
T | null | undefined,T | undefined, orT | nullappearing in well-formed definition files, we can certainly revisit the topic and see if it makes sense to introduce aT?shortcut.@tinganho I’m not concerned about abuse so much as precision. I want a type checker, type system and type language that is precise (and encourages precision).
With
T? = T | null, every combination fornullandundefinedcan be written in shorthand:x: Tx: Tx?: Tx: T?x?: T?For
T | undefined, the?against the identifier rather than the type is correct because “may beundefined” means the same thing as “may not exist” and “may not be set”. It reflects JavaScript’s predefined meaning forundefinedas the value of things that don’t exist or weren’t set.The only thing lacking is a shorthand for
T | undefinedif you need it as a standalone type. But ifundefinedandnullare being used correctly, you won’t actually need it.@aleksey-bykov You said yourself “undefined it’s a bit different because the JavaScript runtime chose to use it for certain situations” and now you’re saying “distinction between null and undefined is an illusion nourished by ignorance”. Are you actually going to bother making an argument or just continue spewing angry gibberish?
arument about dual meaning to undefined is sucked out of the thin air, whenever you see null you have no clue why it is there, null can’t encode anything but a single bit of information, when it comes to a reason why null is there in the first place it just leaves you guessing
if the reason as to why a value is missing is important than null is a poor choice because as was said it doesn’t have any expressive power but to state its presence
one should consider an enum or more elaborate structure to encode different possible reasons if they matter
distinction between null and undefined is an illusion nourished by ignorance
null has no standard meaning, who ever says it does is living in the dark On Mar 11, 2016 10:10 PM, “Jesse Schalken” notifications@github.com wrote:
to all you guys who keeps arguing about meaning of null and undefined, I hope you realize that those are just 2 values no different from 427 or ‘hello world’
you can as well use the string ‘this value is missing’ for what you typically need null
although for undefined it’s a bit different because the JavaScript runtime chose to use it for certain situations
there is no intrinsic meaning to null other that what you want it to be, because it is just a value, just like you can use 634 to secretly call you boss a dumbass in your emails
the only thing that makes null type/value special is that there are almost no operations defined on it (other that equality, inequality and coercion to string), these scarce capabilities are a perfect excuse for choosing it to encode a missing value, a missing value is encoded by the least capable type
but again it only a matter of habbit, there is no thing on earth that stops you from using it to encode something completely different, like you dog’s name: whenever you see null in my app you should read it “smokey”
let’s please avoid making it a done deal that T? is T | null, because if you think null is a special value that can only mean the absence of thereof, you are wrong, null only means whatever you want it to be
in this regard undefined is a much better option, because it’s not you, but JavaScript runtime that makes it different by giving it a special meaning of a missing value On Mar 11, 2016 8:43 PM, “Jesse Schalken” notifications@github.com wrote:
@malibuzios We have indeed considered permitting the
?name modifier in other places. In particular:let x?: number).class Foo { x?: number }).function val(x?: string)?: number).[x: string]?: Entity).The effect would be to include
undefinedin the type, just like optional parameters and properties.I think allowing
?on variable and class property names has a lot of merit. Permitting?on signatures would be nicely symmetric, but its meaning would perhaps be less obvious.@aleksey-bykov If I may chime in… and this may be a bad idea…
You’ve definitely made your point about not liking
T?, but you should probably acknowledge the relative weakness of your argument… if you don’t like the feature, you don’t have to use it in your own code (as you’ve pointed out,Nullable<T>orT | nullwill work just fine) and can even submit a PR for TSLint for a rule to prevent its usage.This means you really don’t benefit much from not having this feature included, unless the continued bikeshedding leads to a delay of the entire nullability system itself (which, as far as I can tell, is not what is happening now, that PR looks a pretty long way from done). On the other hand, those who would prefer a more concise syntax would be left without one if you “win” this argument - a lose-win which could have been a win-meh.
Everyone’s perspective here is important in debating such a complex language feature… but I’d like to see the discussion be more positive so I at least can feel like a happier person while reading it. There’s no reason to be angry about this.
So we have 4 different perspectives
Perspective 1
Great, but there are some cases where
undefinedis unavoidable.x != null!== undefinedConclusions:
T?meansT | undefinedand there is no shorthand forT | null, that would be annoying. Additionally many DOM APIs would have verbose looking type definitionsT?meansT | null, and there is no shorthand forT | undefined, this is generally okay. There are still however cases where you will need to useT | undefinedlike e.g. property access of any arbitrary dictionary.T?meansT | null | undefined, we would have a problem when using strict null comparisons likex !== null. That would fail to eliminateundefined. Therefore we would have to either useT | nullorx != null, which can be substantially slower for high performance code.T | undefinedPerspective 2
This is a workable strategy but its uncommon. Additionally, the DOM APIs often return null which makes
nullimportant enough to model.Conclusions:
T?meansT | undefined, and there is no shorthand forT | null, that would be mostly okay. Except for the DOM apis, which return nulls and will have verbose types to deal with. Additionally we would would have 3 ways to do the same thing:{x?: string}or{x: string?}or{x?:string?}T?meansT | nulland there is no shorthand forT | undefined, this will be somewhat annoying. While optional arguments and fields are still possible to express, return values that can be undefined and casts to “undefinedables” will be unnecessarily verbose.T?meansT | null | undefined, we would have a problem if we are using strict comparisons with undefined likex !== undefined, which fails to eliminatenull. We would have to choose between non-strict comparisonsx != undefined(can be similarly slower in high performance code) or use T | undefined explicitlyPerspective 3
I use both as appropriate, and would like to differentiate between the two
T?meansT | undefinednot okayT?meansT | nullokay, butT | undefinedhas no sugar. Not necessary in most places.T?meansT | null | undefined- same problem with strict comparisonsPerspective 4
Here non-strict comparison i.e.
x != nullis used and usually there is no differentiation between null and undefined / missing values. Which means:T?meansT | undefinednot okay, null is missing (common node example:cb(null, res))T?meansT | null- slightly better ({x?:number?}can do both), but still troublesome.T?meansT | null | undefined- fine. Additionally there is no problem with default arguments because their type is still different and correctly modelled.So? My codebase uses
nulland neverundefined(except to check optional properties/parameters). I posit that this is correct, becauseundefinedhas reserved meaning in JavaScript as the value of properties that don’t exist and parameters that weren’t passed, whereasnullhas no reserved meaning and is therefore appropriate to use for the purpose of a nullable type.JSON matches this, too, which has
nullas a legitimate value but notundefined, because a property with the valueundefinedis considered the same as one which doesn’t exist (JSON.stringify({foo: undefined, bar: null})==={"bar":null}).When a parameter is not passed, it is
undefined. According to ES6, the correct way to check if an optional parameter was passed is with!== undefinedortypeof x !== 'undefined'. Therefore, “is optional” and “accepts undefined” are precisely the same thing,nullis not a valid value for an optional parameter, and sonullandundefinedmust be tracked separately.This was already specified as outside the discussion by @ahejlsberg:
I think there is no smoothing over the difference between
nullandundefined, or between nullability and optionality, and the question really is whether the shorthand for nullability,T?, should imply optionality. I say that it shouldn’t, because we already have a shorthand for optionality,x?: T, which you can use if you want optionality.I prefer solution 1.
Though, I wonder if anyone have considered separating the meaning of
?for property names and type names??for property/variable/param names meansT | undefinedfor whatever annotated type.?for type names meansT | null.Lets begin with explaining the latter
2.first. I think every JS function that are meant to return typeTshould return a value that corresponds to typeTornull. This is consistent with other browser API:s such asdocument.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 meansT | undefinedright now.undefinedmeans uninitialized andnullmeansnovalue. 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 ofnullandundefined. 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:
One of the downside, is that we must deal with how to handle uninitialized variables today:
The proposed syntax is:
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 | undefinedas: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:Pros
Cons
I prefer
number?as a shorthand fornumber | undefinedrather thannumber | undefined | nullbecause it composes better (you can addnull, it’s impossible to subtract it), and serves as a better indicator of how most builtins function. (I’d rather be sprinklingnumber?'s everywhere rather thannumber | undefined, that’s for sure!). So many builtin structures in JS (object indexes, for one!) only ever introduceundefinedand nevernull- 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?andT??look both short and easy to work with, so I’m all for them, but I’ll easily write outtype Nullable<T> = T | null;andtype 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 anull, which would be bad.OK. Let me answer that, then. Is it wise to take vastly differing actions based on whether a value is truthy (e.g. a
number) vstrue?Then we’ll have to agree to disagree. To me the definition of
nullandundefinedin the spec leaves no doubt to their prescribed usage. Would you care to give me your interpretation of the specs? Can you explain what the difference betweenundefinedandnullis supposed to be?The argument have not “degenerated” into a call to the specs. I think following the specs is the strongest argument to be made in the first place. It has far reaching implications and I think you need a very strong counter-argument not to follow it. Of course, that’s assuming we agree on the meaning of the specs, which we apparently don’t.
The rest is arguing about what programming style is better and let’s face it: there will be no consensus on that. If you want to tackle those arguments just go back to the vexing apis in this comment and make a point how much better using
undefinedis.@jods4
I think the “standard” depends on which person you ask. For more “managed languages” it usually default initialises to
null.But consider C++, where there are no default initialisation:
I think JavaScript just adopted the semantics of C++ with the
undefinedkeyword. That’s why I propose thelet x?: numbersyntax.C++ separates the concepts of undefined behaviour and
nullptr. And I don’t see anything wrong with JS doing it too.Correct me if I’m wrong, C++17 have proposed a
not_nullptr. And it only addressesnullsand NOTundefines? So they where kind of in the same seat that TS are in. If they didn’t merge the concept of these two,nullsandundefines. Then why should TS merge the concept of these two?https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Ri-nullptr
The intuition here is that
declares a possibly-initialized variable that is legal to access before its first definite assignment, as shown here (the return type of the containing function now being
x | undefined). This is “just JavaScript” in terms ofundefinedbeing the well-known value of non-initialized properties and variables.The semantics here are basically identical to a possibly-initialized property of an object
@NoelAbrahams That’s very weak.
nullandundefinedare hardly “historical” as they are still used in pretty much all JS written today. New “modern” APIs still follow the behaviour above to the letter.Map.get()is a recent example; it follows the originalnullvsundefineddesign. If the meaning ofnullwas obsoleted we would have had a different API.ASI is a terrible thing yet TS still follows the standard to the letter… so that’s not a precedent for diverging from JS, even though it’s horrible. Rather an argument that TS complies with JS specifications.
Is there any hazard in changing the code to:
When the definition author did :
They really should have done:
I think this is what lazy authors would do … but maybe not 🌹
My primary fear is that lazy .d.ts authors will be like
and break correct code that says
getCompany example doesn’t prove that T? needs to be T | null
it only testifies that 2 different reasons for a missing value have to be considered
first reason (optional parameter) is naturally taken care of by JavaScript
second reason is business related, and up to the developer as to how it should be represented/encoded
you might think that null is a natural choice for it, very well might be so, but if this result is chained with something that expects null for a different reason then you are in trouble
my take that getCompany result might be declared as Company | null no problem but this special case where a missing value as a distinct flavor can’t be used as a universal recipe
a better solution is to add special types whose only purpose is to serve certain reasons for a missing value that can’t be intermixed
getCompany () : Company | CompanyNotAsigned | CompanyNotApplicable
or getCompany () : Company | NoCompany
const enum NoCompany { NotAssined, NotApplicable}
this way you:
@NoelAbrahams Read https://github.com/Microsoft/TypeScript/issues/7426#issuecomment-195743448. Boy does it grind my gears when people dismiss solutions to concrete problems as too “academic”. Using
undefinedfor nullability can and eventually will break your code. If you want broken code, fine, but don’t expect the language and the other people who use it to bend to accommodate it.@jesseschalken You’re dismissing all drawbacks…
About
'a' in x“it’s not checked by TS”. No but if I declarelet x: { a: T | undefined }I can expect'a' in xto always be true. Your suggestion breaks that.About
arguments.length“it’s ES5, ES6 does it differently” There is no ES6-compliant browser available today. Lots of code and people still use ES5 style. There’s no reason to break their code yet. If I declaref(x: T|undefined)I can expectarguments.length == 1. Not with your suggestion.“people who use
undefinedfor empty are wrong” Probably, but lots of people do so, their programs work and TS should support them.The drawback you mention is additional complexity in the compiler but it’s the opposite. Signature of functions already have an optional flag on parameters (could not do otherwise before 2.0 anyway) and
undefinedtyping works at the type checker level with union types. What you want needs to modify TS to mix the function signature with the actual type of its parameters. That’s additional complexity.The drawback I see is loss of clarity. I hate that when I show you this piece of code:
f(x: MyType)you can’t tell me if the parameter is optional or not, unless I give you the precise definition ofMyType. That feels wrong.I don’t get why you don’t want to mandate that optional parameters are declared as such? It’s a matter of type definition in your code or in the
.d.tsdeclarations. If you want a parameter to be optional, say so.f(x?: T)clearly communicates intent, keeps function signature and typing separate and is shorter than the alternative notation.Also right now the nullability debate is only localized to this github thread. If you add:
The debate will span to all TS user.
After thinking about it. I think adding these types might be a mistake?. I guess you want add these to test the waters? Wouldn’t that eliminating the opportunity that
T?meaningT | null. Because after the market have chosen. And let say thatNullable<T> = T | nullbecomes a clear winner with a clear majority. There will be still those who have writtenOptional<T> = T | undefined. Thus you cannot addT? = T | null, because that would markOptional<T> = T | undefinedas a faulty solution. Then the people who have written tells you it was you who added it. How can it be wrong? Let say that you tell them that they don’t need to rewrite. Then for those who useT?will be very frustrated that there existsOptional<T>a little bit everywhere.I think it is best if the TS team bless one solution. Because non-null typing is already a breaking change. And if executed correctly we only need 1 rewrite. Though if you choose to test the waters people would need 2 rewrites.
For me, although a little bit biased,
T? = T | nullwas a clear winner in this discussion.And for your Twitter poll. Although I think you framed the question a little bit wrong. Because honestly I don’t think I understood the question totally or the question was a bit too ambiguous. People still chose a strong preference for
null.IMO TS should bless whatever JS have blessed. And looking at the evidence I think you cannot deny that you find
T? = T | nullto be truth.So there is strong proof and there is empirical result suggesting
T? = T | null. I don’t think you can ask for more?@jods4 Oh, if you check
arguments.length, yeah, but that’s cheating. ES6 specifies=== undefinedas the correct way to check an optional parameter, notarguments.length.The reason not to is that “includes
undefined” and “is optional” then have to be tracked as distinct concepts, complicating the type checker and language semantics, to check something that actually makes no difference at runtime in an effort to help people do something that they shouldn’t be doing in the first place.Calling
f(x: any)asf()is debatable. The whole point ofanyis to opt out of checking, so you can’t expect TS to flag it. If you’re usinganyas the type of something you’re really in no position to claim TS should be throwing errors regarding it.Calling
f(x: {})asf()on the other hand should be an error, and it is if I’m reading #7140 correctly.@jesseschalken
I’m probably nitpicking but no, not exactly. Some illustrations:
And even if passing
undefinedis mostly the same at JS level, I find it more clean that the TS typing clearly reflects if a parameter is or is not optional, even if allowed values include passingundefined. Even if you think some people are “wrong” usingundefinedinstead ofnull, TS should help them if there’s no compelling reasons not to.Final food for thoughts: in this case
f(x: any), I assume you consider the parameter to be optional sinceanyincludesundefined? In TS 1.8 callingf()is an error…EDIT: couldn’t resist adding one more… Generics. Assume
f<T>(x: T), is it consistent thatf<number>()is an error, butf<Optional<string>>()is not? You could say yes but I find it a bit unsettling, especially in the presence of type aliases.A parameter that is not passed in JavaScript is given the value
undefined. A property that doesn’t exist producesundefinedwhen read. Therefore “is optional” and “may beundefined” are precisely the same thing. If the type checker treatedf()andf(undefined)differently, it would imply that they are different at runtime, but they are not. That would be unintuitive.If
nullandundefinedare being used correctly, I can’t imagine a type alias includingundefinedbeing used. In fact, I can’t imagineundefinedoccurring in a type expression at all, since it has shorthandx?: Tfor the places where it’s appropriate.So it’s not necessarily that
f(x: Something)could be calledf()that is unintuitive. It’s the fact thatSomethingincludesundefinedthat’s unintuitive. The developer is only going to get confused if they are already down the wrong path of treatingundefinedas a legitimate value for their own use rather than as reserved for use by the JS runtime.Meaning of null and undefined
OK, let’s try to put a stop to the “meaning” of undefined and null madness. From the ECMAScript Language Specifications
Of course you can do what you want in your code but this is the intended usage of
undefinedandnullas prescribed by JS itself. I feel like TS should support any style of code, but encourage idiomatic JS.Response to criticism of
T? = T | nullFrom the TS design meeting notes #7488
The fact is, “regular” developpers are confused about this topic because, well, JS is confusing. People here in Github are not representative, ask around you what the differences between
if (x),if (!!x),x == undefined,x === undefined,x == null,typeof x == "undefined"andx == void(0)are.TS has an opportunity to improve the situation by prescribing a clear meaning to those types. Assume
Type?to be a nullable type, which – as the name suggests – meansT | nullandparam?to be an optional parameter/member, which can beundefined… it seems to me this is rather logical and could be applied without too much thinking by newcomers. And those people won’t try to mix and match both, which is a huge mistake.There is a common theme of
?meaning “optional”. For a value, this means being nullable. For an identifier this means being undefined. Other than that, there should be no symmetry betweenx?: numberandx: number?because they are not the same thing at all!This is no more confusing than the upcoming
thistyping, wherex(): numberandx: () => numberare going to have a different meaning…What the real problem actually is
Reading other people’s opinion, especially from the TS team, I think that the real problem is that you can’t please everyone.
Today, JS usage in the wild is very varied. Many devs don’t even know about
undefinedor its difference withnulland use them with no clues. Other devs make informed, deliberate decisions to use one or the other. It turns out your program will work in both cases.TS support of
nullandundefinedtypes will model JS faithfully and they will work well for everyone. 🎉The problem is that we want syntactical sugar for our own use-case and this is not the same for everyone. For instance, I use
T | nulland I think it’s the “true” JS design, but if this is adopted, TS team (and others) will be unhappy because they won’t be able to use it in their existing code base and vice versa.If TS tries to please everyone, the result will be a confusing mess. For example:
T?,T=, etc… This look cryptic and which is which? For newcomers: which one should I use? Let’s have fun and mix them in the same project (please don’t)!?on other TS declarations to addundefined. This is a hackish, ugly and confusing workaround. It makes sense where it exists today (e.g. optional parameters), not so much in “optional local variables”?Going this way is poor language design and won’t help new developers. Also, I think we should not try to come up with shorthands that cover every mix’n’match because a codebase should only use one single style.
Some ideas
T?. Every style is supported the same way but none is great. 😦T? = T | null | undefined. But this is poor design as it doesn’t reflect the real expected type of your code. When I doif (x !== null), I would like to haveTnotT | undefined(given it was declaredx: T?). 😦type Nil<T> = T | null; type Un<T> = T | undefined; type Void<T> = T | null | undefined(could be named better). So that alternate coding styles don’t feel too much left out, yet they are not first-class citizens.T?definable intsconfig.json+ in the.d.tsfile pragma. I don’t think it’s elegant, but I think it’s the only sane way to please everyone.Other ideas?
If that were a valid reason not to introduce new syntax, then no new syntax would ever be able to be introduced, because you could always say “but what if something better comes up and we can’t undo it?”. Doesn’t seem very constructive.
(And
nullis a unit type, not a bottom type.)A question mark after the identifier,
x?: T, means the identifier may not actually exist or be set, so reading it may produceundefined.A question mark after the type,
x: T?, means the identifier will exist/be set, but the value itself is nullable, so reading it may producenull.It’s pretty intuitive to me.
@mhegazy @RyanCavanaugh @weswigham @ahejlsberg
This question is directed to all Microsoft members who have participated or followed this thread:
You started the discussion specifically with the purpose of finding semantics for
T?(or any of its variants), i.e. a shorthand type expression that would cause the receiving type to include eitherundefinedornullor both in a union.However, you already have an shorthand expression in the language to denote
T | undefined:Have you considered simply extending it to
let,varand class property expressions (or others that I might have missed)?Why wasn’t this proposed even as an option for consideration? Wouldn’t that be a natural and intuitive extension of the existing syntax?
Sure, this is not really a “complete” solution because it doesn’t allow expressing a type that, say, could be passed to a generic expression or set as a function return type (though that could be handled by type inference in some cases), as would be possible with a
T?e.g.:but still it would cover a lot of common cases and may be worth considering, if only for its consistency and uniformity with the existing syntax of the language?
Example: It would give a nice symmetry between an interface declaration and its implementing class:
@basarat I don’t see the purpose of an optional class property. If you want a nullable class property you can use
foo:number? = null. Nonetheless, if you want an optional property I don’t see why thefoo?:numbersyntax couldn’t be permitted in classes as shorthand forfoo: number | undefined.Insofar as
(new class { foo:number; }).foobeingundefinedis a problem, the problem is specifically a lack of checking for initialisation of properties, which is a distinct issue in and of itself.Of course, I do use
undefiendto check existence of an optional parameter or optional property, but this is always in correspondence with ax?: Tproperty/parameter declaration, so I don’t suspect I’ll need to writeT | undefinedas a type by itself very often. On the other hand, I will often need to writeT | nullfor things that are nullable, so a shorthand for that would be handy.I can only imagine needing
undefinedas a type if for some reason (not sure why) I want to be precise about the result of a void function, for which the typevoidunnecessarily includesnull, which is unfortunate.@aleksey-bykov sorry about that, was just trying to diffuse the strangeness of my proposal with some light-hearted comments.
I think the issue (I was trying to show) is no matter what
T?means, at least one group of users wont find it useful, and its kind of hard to make syntax sugar for everyone without having it look a little ridiculous.Maybe the original
T? = T | null | undefinedis the best after all. Its the longest union type (and therefore in most need of shortening) and for those situations where it needs to be more specific,T | undefinedandT | nullwill do.I’d like to make the following:
A modest proposal
?TmeansT | undefined,T?meansT | null,?T?meansT | null | undefinedThe users that only use
nullwill simply useT?whenever possible (except for optional arguments). They will almost never encounter things of typeundefined. For them the only optional type that exists will beT?The users that only use
undefinedwill simply use?Teverywhere. They probably don’t use the DOM anyway if they manage to avoidT?. Therefore they get the same priority treatment as group 1 with a slightly different syntax.The users who use both
nullandundefined, each where appropriate will be able to take the full advantage of this newfound type system expressiveness. The intuition of the question mark position will be used to guide them in choosing the correct type: if you squint a little,{t?:?number}looks like it only has a single question mark.The users that don’t care about the differences between null or undefined will probably be slightly annoyed. To let them display their annoyance in a more expressive way
T??can be added. To further enhance expressiveness, a liberal number of “!” and “?” characters can be allowed between the two question marks, resulting with types such asT?!?,T??!?!?and others.@malibuzios this is not compatible with ES6 default function arguments - according to spec, only
undefinedis recognised as a missing value but notnullisn’t@ahejlsberg
I was glad to read that. I found the introduction to the PR perfectly fitting for the use-cases we have.
I vote for
2. T? means T | undefined.We do not use null anywhere in the code-base. I don’t quite like adding
??as an additional operator. The longer union type notation can be used if necessary.Goes without saying big 👍
Note that the compiler source code seems to treat nullability and optionality as the same concept (by using
undefinedonly and nonull). Or perhaps nullability is not a concept that’s used at all¯\_(ツ)_/¯Just look at all thereturn undefinedstatements in : https://github.com/Microsoft/TypeScript/blob/01c329c05ed52bd13ff1ba48bc673c145ebc9a8f/src/compiler/utilities.tsThe return signatures for these would be
T | undefined(sincenullis not used).Also a some functions in that file take
undefinedas a valid argument without actually saying that the argument is optional. Optional arguments are a concept we already had in TypeScript argument signatures but were not used here => concepts of nullability / optionality were merged.Also lots of JavaScript code bases use both
nullandundefined(when people try to differentiate the two concepts they end up using both) and might be painful to get these right every single time 🌹I like @RyanCavanaugh’s proposal of
T? = T | null. That’s the type I usually want when I want something “nullable”.I don’t think it’s good to use one character for
| null | undefined. I’ve seen many bugs resulting fromundefinedsneaking into a “nullable” parameter or property, where there probably just wasn’t any thought put into whetherundefinedshould be allowed. The typical scenario:Xwith a “nullable” property/parameterpXmay producep: null, but there’s one obscure place where it may be produced withp: undefinedXcan handle eitherp: nullorp: undefined, but there’s one obscure place which breaks withp: undefined@tejacques your first concern seems to be covered by the PR (non-nullable variables cannot be used before they’re assigned a value)
I think it comes down to the question:
The only thing I can think of is high performance code, i.e. its beneficial to force object field presence to avoid extra hidden classes and to enforce monomorphism: https://jsperf.com/monomorphism-with-nullables-3
@tejacques I am not buying the argument about code being more standardized by embracing one notation or another, because at the end of the day it’s a union type, which means you can alias it
in one library, then
in the other library, and then go plain
in the third library
and guess what? all 3 of them are going to be compatible as far as the contract
Regarding Ryan’s comments about the prevalence of null: Google’s JavaScript style is to use null (and not undefined) as the sentinel value. We have ~2.5x as many occurrences of “null” over “undefined” in our internal codebase across all products (excluding third-party JS). (I wanted to comment here to quantify how often
T|nullvsT|undefinedappear in the code, but Closure’s type system makes that difficult to grep for.)So on the purely selfish argument, I think options 2 and 3 aren’t useful for us. (We mix TS and JS code, translating types between TS and Closure types at the boundaries, so our TS code will surely also use nulls heavily.) I find Ryan’s argument pretty convincing so from the list I like option 5. (Or option 4: I think the majority of our uses of undefined are in default arguments, which TS has other syntax for already.)
I appreciate that the TS codebase style went the other way though! And while it’s possible to write
type Opt<T> = T|undefined;I imagine you probably don’t want to actually do that to all of your code…Because this bikeshed is purely about sugar, and the point of sugar is to huffman encode frequently-occurring patterns, I think your ultimate decision should be dictated by which things occur most frequently – you don’t necessarily need a short encoding of every possible permutation of types. For example, do you envision that DOM/browser APIs will be written with
Nullable<T>sprinkled everywhere?Maybe it’s worth skimming through DefinitelyTyped to see which libraries would benefit from what? https://github.com/DefinitelyTyped/DefinitelyTyped/search?utf8=✓&q=undefined+-filename%3Atests+-filename%3Atest&type=Code
@spion
x?: number?syntax doesn’t work as well for standalone types@spion relying on
?'s on initializers for one of the unions would mean there’s no shorthand for casting to that form.Ok, so we now have five options:
T?meansT | null | undefined.T?meansT | undefined.T?meansT | undefinedandT??meansT | null | undefined.T?meansT | null.T?meansT | nullandT??meansT | 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:
value?: Tthe way the areT | null | undefinedvar value: string | null | undefined;ortype Nullable<T> = T | null;if you ask me the
var value: T | null | undefinedis as clear as a day, whereasvar value? : T??is what I type when I am hungoverthe argument that
T | null | undefinedis longer to type is faint whoever wants can make it as short asJust to explore what this means
Option 1:
T?=T | undefinedThis is an argument for
T?being exactly equal toT | undefined.Consider some code:
Should
fandgbe identical in behavior when invoked with one argument ? Intuition seems to point to yes. Ifnullwere in the domain forxing, then these functions would not be equivalent, which is potentially very confusing.Now let’s think of the implementation of
g:Is this code correct? Intuition, at least among people who don’t use
null, says “yes”.Similarly, for types:
If
nullwere in the domain ofnumber?, thenhcould beundefinedornumber, butwcould beundefined,number, ornull. Again, all things equal, symmetry is very preferred here.Pros
(x: number?) => voidand(x?: number) => voidnullundefinedis the only true “missing” value in JavaScript (modulo some obscure DOM APIs)Cons
nullas a sentinel non-valueOption 2:
T?=T | undefined | nullThis is an argument for
T?being exactly equal toT | undefined | null.Consider some code:
Is this code correct? It certainly looks like it. But if
T?isT | undefined, then this program has two errors -fhas an incorrectreturn nullcodepath, andxcan’t be compared withundefined.Also consider this code:
Again, at inspection, this program looks good.
Pros
nullCons
(x: number?) => voidand(x? number) => voidOption 1a: Include
T??=T | undefined | nullIf
T?=T | undefined, then we might want an alias forT | undefined | null; given available punctuation,??seems like the best choice (T~andT*were mentioned but considered generally worse).Pros
Nullable<T>Cons
T??looks somewhat silly, at least for nowT??isn’t the same as(T?)?