[Request for feedback] Nullable types, `null` and `undefined`
mhegazy opened this issue ยท 162 comments
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:
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
I thought that nullable Foo
meant Foo | null | undefined
i.e. both null
and undefined
are considered valid nulls.
Belief basis:
From #7140
That PR has instances in the discussion where it means only undefined and sometimes both null and undefined. I think Nullable should mean one of these consistently everywhere. Sorry if I've misread ๐น.
Just to explore what this means
Option 1: 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 nowT??
isn't the same as(T?)?
- Maybe no one uses this and it's a waste of effort / complexity
@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
)
As @RyanCavanaugh, this issue is to illicit feedback to put in #7140; so #7140 should not be used as the expected behavior yet.
My vote: Allowing null
to be a valid nullable aka T | null | undefined
Reasons:
null
is used quite a lot in node forerror
argument to callbacks.null
is in the name of nullable- flow treats
null
andundefined
the same : http://flowtype.org/docs/nullable-types.html
@RyanCavanaugh looking back I did originally misread the question here ๐น :)
From a developer ergonomics perspective, 99% of the time I care about nullability it's because I want to know "is it safe to call 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!
Another use case to consider:
let map: {[key: string]: MyObject?} = ...;
let x = map['foo'];
x.bar();
I vote for option 1 as described by @mhegazy but I'd like a clarification.
Are there any problems with the lack of symmetry between (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 outx?: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...
In case someone finds this useful there's a similar proposal for the dart language here (Non-null Types and Non-null By Default (NNBD)).
It includes a comparison to null types in other languages.
I prefer 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.
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:
x
has a value (T
)x
might be missing, but if it's not missing, it has a value (T | undefined
)x
is not missing, but might not have a value (T | null
)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
. Whenx
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.
@RyanCavanaugh That makes a great deal of sense. ๐ (null
for things that are nullable. undefined
for things that may not exist.)
Also, from a purely DX perspective, if null
and undefined
are conflated (eg bar: string?
=== bar?: string
, or the former overlaps the latter), it is easy to accidentally omit properties or arguments just because they happen to be nullable. Eg:
function func1(params: {foo: string, bar: string?}) {
// ...
}
function func2(obj: Thing) {
let foo = obj.getFoo()
let bar = obj.getBar()
// ... use foo and bar ...
func1({
foo: foo
// oops, forgot to pass bar
})
}
TL;DR: Flow was right, T? should be T | null. Seeking symmetry around x?: number and x: number? is a trap.
The page @basarat linked to (http://flowtype.org/docs/nullable-types.html) says Flow considers ?T
effectively as T | null | undefined
. Is that right?
@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
}
y
andz
are the same type and behave the same way{w: null}
is assignable toFoo
, omitting bothx
andy
which use the existing "optional" property syntax, andz
which uses the new "super-nullable" (for lack of a better name) syntax- There is a distinction between optional (or "undefinable") properties like
x?: string
and nullable properties likex: string?
. {}
is not assignable toFoo
, it's missingw
. Similarly, neither is{x: "bar"}
.
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 areT | 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;
ortype 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;
If you want another example of prior work other than Flow, in JSDoc
comments for the closure compiler, T?
actually equates to T|null
while
T=
is T|undefined
.
On Tue, Mar 8, 2016, 9:57 AM Aleksey Bykov notifications@github.com wrote:
if you ask me the var value: T | null | undefined is as clear as a day,
whereas var value? : T?? is what I type when I am hungoverthe argument that T | null | undefined is longer to type is faint whoever
wants can make it as sort astype u = undefined;
type n = null;
var value: T | n | u; // <-- fixed!โ
Reply to this email directly or view it on GitHub
#7426 (comment)
.
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 | undefined
for 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 type T
should return a value that corresponds to type T
or null
. This is consistent with other browser API:s such as document.getElementById('none'); // null
. This is also consistent with other programming languages.
For 1.
. There is already a way of defining optional params and properties in Typescript. And it means T | undefined
right now.
undefined
means uninitialized and null
means novalue
. Checkout this SO thread http://stackoverflow.com/questions/5076944/what-is-the-difference-between-null-and-undefined-in-javascript. Many programmers that come from other programming languages have hard time to separate the meaning of null
and undefined
. Though I believe that pure JS developers don't have this problem.
C++ also default initializes to undefined behaviour and not null
. Javascript is just a copy of that.
Also quoting one of the comments of the first answer in the SO thread:
You may wonder why the typeof operator returns 'object' for a value that is null. This was actually an error in the original JavaScript implementation that was then copied in ECMAScript. Today, it is rationalized that null is considered a placeholder for an object, even though, technically, it is a primitive value.
One of the downside, is that we must deal with how to handle uninitialized variables today:
let s: string;// string | undefined
The proposed syntax is:
let s?: string;// string | undefined
So there is quite a LOT of code that must be rewritten. BUT probably as many lines as any non-null proposal.
Also one can define T | null | undefined
as:
let s?: string?;// string | null | undefined
One other downside is, there might be a few people who have written function that returns T | undefined
. In my view this is already broken in the first place. Though I think it is extremely rare. One could provide an escape hatch to define the correct type in those cases:
function getString(): string | undefined {
if (Date.now() % 2 > 0) {
return 'hello world';
}
return undefined;
}
Pros
- consistent with the current JS and TS semantics.
Cons
- Breaks existing code.
there is one more problem with the current semantics of value?: T
, namely:
- it doesn't require being initialized
and it's a huge PITA, because turns out it's not the same as value: T | null | undefined
that has to be initialized, is it?
A few assertions to ground this discussion:
- The semantics of
T | null
,T | undefined
, andT | null | undefined
are not in question and we're not proposing any changes from what is described in the introduction to #7140. - An optional parameter or property declaration automatically adds
undefined
(but notnull
) to the type of the parameter or property (i.e.x?: T
is the same asx?: T | undefined
) and we're not proposing changes to this.
The purpose of this discussion is only to debate which of the following shorthand notations we want:
T?
meansT | null | undefined
.T?
meansT | undefined
.T?
meansT | undefined
andT??
meansT | null | undefined
.
In other words, we're bikeshedding on the meaning of the ?
type modifier and nothing else.
Option 1 is where we're currently at, but we were simply curious about people's opinions on 2 and 3.
I should mention that it is of course always possible to define your own preferred type notation using generic type aliases. For example:
type Opt<T> = T | undefined;
type Nullable<T> = T | null;
To summarize @RyanCavanaugh proposal in the same terms:
4
. T?
means T | null
.
5
. T?
means T | null
and T??
means T | null | undefined
.
you forgot the 3rd case: when I need T | undefined
I go... T | undefined
correct?
@Aleksey-Bykov as @ahejlsberg mentioned, T | undefined
and T | null
are always valid. you can use them whenever you need or like. the question is what the shorthand notation T?
and possibly T??
would mean. null
and undefined
are now valid type names.
Ok, so we now have five options:
T?
meansT | null | undefined
.T?
meansT | undefined
.T?
meansT | undefined
andT??
meansT | null | undefined
.T?
meansT | null
.T?
meansT | null
andT??
meansT | null | undefined
.
you cannot put 3 pigeons in 1 cage, i vote for 1: T? means T | null | undefined
at least it's aligned with the current semantics of optional parameters/properties
although can't see what sort of a well supported argument can be made here
Can we add option 6, have a modifier for both?:
T?
means T | null
, T=
means T | undefined
, and T?=
or T=?
means T | null | undefined
. This would mirror the closure compiler's syntax for nullable/optional.
Do we really need T??
though? Assuming we choose something like option 5, I wager that T??
will actually be very rare. In function arguments you can use:
(x?: T?)
instead, and in record fields you can use
{x?: T?}
instead.
Which AFAICT means the only place where a T??
annotation can appear is for un-inferred local variables.
For that case, could we not use the following syntax?
var x?:number?;
Reads: Local variable x
may or may not "exist", and if it does, its either of type T or null.
Another advantage is that the sugar also works for a local variable that can be undefined (but not null):
var x?:number;
Unless I'm forgetting something?
@spion relying on ?
's on initializers for one of the unions would mean there's no shorthand for casting to that form.
@spion x?: number?
syntax doesn't work as well for standalone types
@weswigham / @Aleksey-Bykov yeah, it seems I forgot about casting.
edit: examples of casting / standalone types would be useful - I can't think of any realistic ones.
@spion relying on ?'s on initializers for one of the unions would mean there's no shorthand for casting to that form.
@weswigham can you give an example?
@spion, there are no realistic ones because there is no proposed syntax for standalone types
what i meant is whenever you need to make a match to the type of the x
property in { x?: T? }
you would need to do T? | undefined
to my eye it looks like the syntax fell short
I mostly agree with @RyanCavanaugh. I don't think that x?: T
and x: T?
are the same thing or should be the same thing. However, I see some potential issues with using T? = T | null
:
- Declaring a
x: T?
variable requires assigning it (sinceundefined
is not a valid value), it becomes considerably harder to use it, because there could be missing assignments which are not caught. - If there was concern that different semantics of
x?: T
andx: T?
would cause confusion, the introduction of a thirdx: T??
certainly will. In that language,x?: T
isdefinable
,x: T?
isnullable
andx: T??
isvoidable
. This model is most accurate I suppose, but I don't think that it is most practical. @ahejlsberg pointed out that there are now 5 different things to choose from.
Personally, I'm in favor of @ahejlsberg/original issue Option 1. If users want to have more control, and are able to declare types like type Nullable<T> = T | null
and type Undefinable<T> = T | undefined
I think that's a reasonable approach when more control is needed.
seriously no one ever questioned why T?
needs to be a thing, let's ditch this idea before it's too late and stick with the unions for, as it was said many times, the question-sign-syntax is 1 - incomplete and 2 - optional which makes it a spoon of syntax sugar, did anyone ask for it?
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|null
vs T|undefined
appear 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=%E2%9C%93&q=undefined+-filename%3Atests+-filename%3Atest&type=Code
@Aleksey-Bykov there is no such thing as var value?: T
, that notation can only be used to denote optional function arguments.
Edit: Looks like you removed that notation.
I think there is value in a baked in alias of T? = T | null | undefined
because otherwise you can't declare a Maybe/Optional type without an alias outside of function args. Having a standardized way of doing this is useful because it means code will be more uniform.
@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
Nullable<T> = T | null
in one library, then
Opt<T> = null | T
in the other library, and then go plain
var valute: null | T;
in the third library
and guess what? all 3 of them are going to be compatible as far as the contract
This would mirror the closure compiler's syntax for nullable/optional.
Closure treats object types are nullable by default and require an annotation the other way, so there would be a mismatch for such types between the TS annotation (Foo
, Foo?
) and the corresponding JSDoc annotation (!Foo
, Foo
) anyway.
@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:
Is there any gain in forcing that a value (an argument or an object field) must be present but is allowed to be null ?
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
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 from undefined
sneaking into a "nullable" parameter or property, where there probably just wasn't any thought put into whether undefined
should be allowed. The typical scenario:
- There is a type/method
X
with a "nullable" property/parameterp
- Many producers/callers of
X
may producep: null
, but there's one obscure place where it may be produced withp: undefined
- Most consumers/implementors of
X
can handle eitherp: null
orp: undefined
, but there's one obscure place which breaks withp: undefined
- The two incompatible sides never meet during testing
I vote for 4 or 5, T? = T | null
, because I agree that nullability and optionality are separate, orthogonal concepts, corresponding to values null
and undefined
respectively, 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 with x?: T?
(or x: 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 with T?
instead of T | null
.
Note that the compiler source code seems to treat nullability and optionality as the same concept (by using undefined
only and no null
). Or perhaps nullability is not a concept that's used at all ยฏ\_(ใ)_/ยฏ
Just look at all the return undefined
statements in : https://github.com/Microsoft/TypeScript/blob/01c329c05ed52bd13ff1ba48bc673c145ebc9a8f/src/compiler/utilities.ts
The return signatures for these would be T | undefined
(since null
is not used).
Also a some functions in that file take undefined
as 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 null
and undefined
(when people try to differentiate the two concepts they end up using both) and might be painful to get these right every single time ๐น
Note that the compiler source code seems to treat nullability and optionality as the same concept (by using undefined only and no null).
So? My codebase uses null
and never undefined
(except to check optional properties/parameters). I posit that this is correct, because undefined
has reserved meaning in JavaScript as the value of properties that don't exist and parameters that weren't passed, whereas null
has no reserved meaning and is therefore appropriate to use for the purpose of a nullable type.
JSON matches this, too, which has null
as a legitimate value but not undefined
, because a property with the value undefined
is considered the same as one which doesn't exist (JSON.stringify({foo: undefined, bar: null})
=== {"bar":null}
).
Also a some functions in that file take undefined as a valid argument without actually saying that the argument is optional.
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 !== undefined
or typeof x !== 'undefined'
. Therefore, "is optional" and "accepts undefined" are precisely the same thing, null
is not a valid value for an optional parameter, and so null
and undefined
must be tracked separately.
This was already specified as outside the discussion by @ahejlsberg:
A few assertions to ground this discussion:
- The semantics of T | null, T | undefined, and T | null | undefined are not in question and we're not proposing any changes from what is described in the introduction to #7140.
- An optional parameter or property declaration automatically adds undefined (but not null) to the type of the parameter or property (i.e. x?: T is the same as x?: T | undefined) and we're not proposing changes to this.
I think there is no smoothing over the difference between null
and undefined
, 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 wonder if it makes sense to remove T | undefined
and T | null
from the list? Because the list must cover both undefined
and null
. And having just one or the either makes no sense in this debate, because you must deal with both.
These are the suggestions so far:
T?
meansT | null | undefined
.T?
meansT | undefined
andT??
meansT | null | undefined
.T?
meansT | null
andT??
meansT | null | undefined
.T?
meansT | null
andvariable?
meansT | undefined
andvariable?: T?
meansT | null | undefined
.T?
meansT | null
andT=
meansT | undefined
andT=?
orT?=
meansT | null | undefined
.
class Foo {
// ...
}
function needs_foo(foo:Foo? = new Foo()) {
// ...
}
If T?
means T | null | undefined
, then the foo
parameter is Foo | null | undefined
on the outside and inside, even though from the inside it is actually Foo | null
since undefined
would be replaced with new Foo()
. So you have to deal with undefined
in your function body even though it wont happen.
If you already have a Foo?
and pass it into needs_foo()
, then you can't predict whether it's going to be replaced with new Foo()
or not, since undefined
will be, but null
wont, and Foo?
includes both.
That's fairly complicated behaviour for the shorthand nullable type.
@jesseschalken this comment is related: #7140 (comment). I think one conclusion is that it is treated as T | null
inside the function for default arguments.
@tinganho I think that's talking about something like (x:string = "hello")
, where the default causes the type for the caller to be string | undefined
, but it remains string
inside the callee.
But if the type itself includes undefined
, eg x:string? = "hello"
(with string?
=== string | null | undefined
), the compiler will have to go one step further to have string | null
in the body, by not only not adding undefined
to the type (as it shouldn't), but also removing it (because it's already there). That may be a reasonable thing to do in any case, but it creates a disconnect between the type the developer has and the type the developer wrote, and for example means they can't assign string?
to the variable even though that's the type they gave it, because the compiler changed it to string|null
.
The semantics of T | null, T | undefined, and T | null | undefined are not in question and we're not proposing any changes from what is described in the introduction to #7140.
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 ๐
I think there are certain types of developers who are willing to accept expressions like:
value?: number?
and
value: number??
or even
value?: number??
as reasonable or even "good" syntax in a modern programming language, regardless of semantics. Some may even describe this as "beautiful". I'm afraid these may be the kinds of people who are attracted to heavily technical discussions in public forums, not necessarily the "average" developers.
I would like to call out the need for simplicity and intuitiveness. I don't believe any of these shorthand notations are great, considering the question mark notation already exists in the language. I believe a function parameter that would also accept a null
would be better off with the number | null
notation:
function doSomething(value: number | null)
function doSomething(optionalValue?: number | null)
is more readable than any of:
function doSomething(value: number?)
function doSomething(optionalValue?: number?)
function doSomething(optionalValue: number??)
function doSomething(optionalValue?: number??)
In this case I see null
being a part of the type itself, rather than denoting optionality. The type of value
is number | null
- this expression denotes a union type, not optionality.
Since the intention of the proposal is to have all variables require assignment by default, a syntactic sugar for T | null | undefined
would still be useful though, to mimic the "normal" Javascript idiom of nullability.
_Edit: I seem to have contradicted myself here suggesting that let variable?: T
would mean variable: T | null | undefined
, I probably meant it would mean T | undefined
, that would be more consistent with the examples I gave for function parameters. This was pointed out by @spion and corrected in a later comment._
I would like propose an additional, more conservative compromise, that hasn't been mentioned yet:
Instead of adding the question mark to the type, e.g:
let x: number?;
add the question mark to the variable name itself, e.g.:
let x?: number;
This would only influence variable and member declarations, as function parameters, object types and interfaces already allow this form, and would parallel both the syntax and, for the most part, the semantics exactly.
I have not given this a large amount of thought at this point (just an idea that came up) and might be missing something here, so I would be very interested in your thorough feedback on this alternative approach.
For those who might have missed the additional syntax/semantics suggestion I've made in my previous comment, I'll try to summarize the ones that were made so far, and include it as well (no. 6):
T?
meansT | null | undefined
.T?
meansT | undefined
andT??
meansT | null | undefined
.T?
meansT | null
andT??
meansT | null | undefined
.T?
meansT | null
andvariable?
meansT | undefined
andvariable?: T?
meansT | null | undefined
.T?
meansT | null
andT=
meansT | undefined
andT=?
orT?=
meansT | null | undefined
.T?
is not introduced at all, onlylet variable?: T
, which would meanvariable: T | null | undefined
. A nullable, but not optional variable type is expressed verbosely asT | null
(or the genericNullable<T>
), and optional (or undefinable) but not nullable byT | undefined
(or usingOptional<T>
/Undefinable<T>
).
@malibuzios this is not compatible with ES6 default function arguments - according to spec, only undefined
is recognised as a missing value but not null
isn't
@spion That's interesting, so you mean that an expression like let variable?: T
would be more naturally seen to you to correlate to let variable: T | undefined
?. That was actually my personal preference, as that seemed to be more inuitive, but I thought for some reason including null
would be more compatible with the Javascript idiom, I guess I might be wrong.
In any case, having only let variable?: T
meaning let variable: T | undefined
might even be better (I mean, still without assigning any new syntax like T?
). To express let variable: T | null | undefined
you could use let variable?: T | null
(or let variable?: Nullable<T>
) which was my preferred style for function parameters.
Removed: a corrected and expanded suggestion list is posted in my following comment
@malibuzios it is not very helpful to introduce a new numbering of options, when everyone else has been referring to the options by number as numbered in #7426 (comment) .
@martine
I based the numbering on the ones by @tinganho at #7426 (comment) . I later realized it was different than the comment you referenced. There seems to be some differences between them. I will try to consolidate them all and preserve the original numbering of the earlier one, here:
T?
meansT | null | undefined
.T?
meansT | undefined
.T?
meansT | undefined
andT??
meansT | null | undefined
.T?
meansT | null
.T?
meansT | null
andT??
meansT | null | undefined
.T?
meansT | null
andvariable?
meansT | undefined
andvariable?: T?
meansT | null | undefined
.T?
meansT | null
andT=
meansT | undefined
andT=?
orT?=
meansT | null | undefined
.T?
is not introduced at all, instead onlyvariable?: T
, which is equivalent tovariable: T | null | undefined
. Nullable but not optional isT | null
orNullable<T>
, and optional (or undefinable) but not nullable asT | undefined
orOptional<T>
.T?
is not introduced at all, instead onlyvariable?: T
, which would be equivalent tovariable: T | undefined
. Nullable but not optional variable type isT | null
orNullable<T>
, and both nullable and optional is expressed asvariable?: T | null
orvariable?: Nullable<T>
.
So we have 4 different perspectives
- I never use undefined, always null
- I never use null, always undefined
- I use both as appropriate, and would like to differentiate between the two
- I don't want to care
Perspective 1
Great, but there are some cases where undefined
is unavoidable.
- testing for an object property, we must compare with undefined or at least weak-compare with null i.e.
x != null
- testing for an optional argument, we must adhere to ES6 specs which will always strictly compare with undefined i.e.
!== undefined
Conclusions:
- If
T?
meansT | undefined
and there is no shorthand forT | null
, that would be annoying. Additionally many DOM APIs would have verbose looking type definitions - If
T?
meansT | null
, and there is no shorthand forT | undefined
, this is generally okay. There are still however cases where you will need to useT | undefined
like e.g. property access of any arbitrary dictionary. - If
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 | null
orx != null
, which can be substantially slower for high performance code. - The type of current optional arguments must be precisely
T | undefined
Perspective 2
This is a workable strategy but its uncommon. Additionally, the DOM APIs often return null which makes null
important enough to model.
Conclusions:
- If
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?}
- If
T?
meansT | null
and 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. - If
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 explicitly
Perspective 3
I use both as appropriate, and would like to differentiate between the two
T?
meansT | undefined
not okayT?
meansT | null
okay, butT | undefined
has no sugar. Not necessary in most places.T?
meansT | null | undefined
- same problem with strict comparisons
Perspective 4
Here non-strict comparison i.e. x != null
is used and usually there is no differentiation between null and undefined / missing values. Which means:
T?
meansT | undefined
not 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.
I'd like to make the following:
A modest proposal
?T
meansT | undefined
,T?
meansT | null
,?T?
meansT | null | undefined
The users that only use null
will simply use T?
whenever possible (except for optional arguments). They will almost never encounter things of type undefined
. For them the only optional type that exists will be T?
The users that only use undefined
will simply use ?T
everywhere. They probably don't use the DOM anyway if they manage to avoid T?
. Therefore they get the same priority treatment as group 1 with a slightly different syntax.
The users who use both null
and undefined
, 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 as T?!?
, T??!?!?
and others.
?T?
this is just embarrassing... seriously? people are going to be pointing at me and saying: look here comes the ?T?
guy, what a shame
seriously 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
@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 "Have T?
be a shortcut for T | undefined
" and "Have T?
be a shortcut for T | null
". I guarantee it. There's also strong precedent here as T[]
is already sugar for Array<T>
(which no one seems to complain about).
@RyanCavanaugh, I can't see a problem that is being solved here, what I see is a problem being made and being tried to solved immediately before even spelled.
I don't buy the slippery slope argument that people are going to be asking for something they never seen before. Why would they? Even if they do the answer can be as short as a link to the spec or an explanation that only needs to be written once. Was this contructive enough?
ยฟT?
syntax can be shipped later when you see that the demand for this feature exists
@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 | undefined
is 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 | undefined
and T | null
will do.
@spion I am with you on that, there cant be 3 pigeons in 1 cage, 2 would have to go home and be pissed, which ones? well, I hope it's not the question of the agenda
@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>
or T | null
will 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.
but there are some cases where
undefined
is unavoidable
Of course, I do use undefiend
to check existence of an optional parameter or optional property, but this is always in correspondence with a x?: T
property/parameter declaration, so I don't suspect I'll need to write T | undefined
as a type by itself very often. On the other hand, I will often need to write T | null
for things that are nullable, so a shorthand for that would be handy.
I can only imagine needing undefined
as a type if for some reason (not sure why) I want to be precise about the result of a void function, for which the type void
unnecessarily includes null
, which is unfortunate.
@dallonf, I am not angry, sorry for making you feel I am,
point is every language has features they wish didn't exist (not to go far, take the with
statement in JavaScript and its proponents)
we have a chance to be responsible and oversee far stretching consequences of our decisions
the way this question is posed is flawed, becase it fails to identify the problem, instead it postulates a need for a new syntax that can't fit a new feature nicely, hence the confusion
so I don't suspect I'll need to write T | undefined as a type by itself very often
consider:
class Foo {
foo:number;
}
const x = new Foo();
console.log(x.foo); // undefined
We don't have the syntax:
class Foo {
foo?:number;
}
const x = new Foo();
console.log(x.foo); // undefined
So you end up with :-/ :
class Foo {
foo:number | undefined;
}
const x = new Foo();
console.log(x.foo); // undefined
Similar for just a raw let foo:number;
< although you can generally convert it into a functional equivalent that initializes it correctly.
Again I am on the side of T | null | undefined
since flow already made that choice for me ๐น
I'm increasingly liking T? = T | null | undefined
, with simple unions for other cases.
To use my previous example, that would look something like this...?
interface Foo {
w: string?, // string | null | undefined
x?: string, // string | undefined
y?: string?, // string | null | undefined, redundant, probably flagged by linters
z: string?? // syntax error ;)
}
let foo: string? = "bar"; // foo is string | null | undefined
There's still a tiny bit of potential confusion between w
and x
; it might be simpler to stick with T | null | undefined
for optional properties as well, but I still kinda like the idea of optional properties being just undefined
- it's probably a safe enough place to model the difference.
After all, what you're saying with an optional property or argument is not so much that undefined
is an allowable value but that the entire property or argument doesn't need to exist - which just happens to be the same thing in JavaScript.
@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 the foo?:number
syntax couldn't be permitted in classes as shorthand for foo: number | undefined
.
Insofar as (new class { foo:number; }).foo
being undefined
is a problem, the problem is specifically a lack of checking for initialisation of properties, which is a distinct issue in and of itself.
They are both equivalent in my mind and they haven't caused me any pain : https://medium.com/@basarat/null-vs-undefined-in-typescript-land-dc0c7a5f240a#.6p40edpr0 ๐น
Whether people use undefined/null
or not, is less relevant then what is currently implemented in browsers.
It seems to me that they have chosen null
. Any depart from that will be weird from my perspective.
I noticed you both mentioned the variable?: T
syntax, which is already supported in the language:
interface MyInterface {
optionalValue?: string;
}
function doSomething(optionalValue?: string);
First of all. Is there any consensus on what the precise semantics of this are going to be after the non-nullability feature is introduced and enabled?
Would it mean that optionalValue?: string
is shorthand for optionalValue: string | undefined
or optionalValue: string | null
or optionalValue: string | null | undefined
or none of that - perhaps just denoting a missing declaration or parameter? (though the function parameter case must have some sort of type associated with it in the body of the function? my guess it would mean string | undefined
?)
I find it very surprising no one mentioned this aspect of the existing syntax because it is very important?
Also, what happened if this syntax would be expanded to let
, var
and class member expressions, i.e.:
let optionalValue?: string;
var optionalValue?: string;
class MyClass {
optionalValue?: string;
}
I see a heated discussion on T?
, but this syntax extension seems more natural to me and could cover a lot of cases. My suggestion was that it would mean T | undefined
and in principal I have nothing against an additional shorthand for T | null
just think it can be postponed for later if this would be available (perhaps using a some other approach than T?
or ?T
etc.).
Why is this left out of the discussion? I haven't seen anyone from Microsoft considering it or even mentioning it. Is there some reason they thought it was a really bad idea in the first place or it simply never occurred to them?
@malibuzios see #7426 (comment) (and #7426 (comment)) above
You mean:
An optional parameter or property declaration automatically adds undefined (but not null) to the type of the parameter or property (i.e. x?: T is the same as x?: T | undefined) and we're not proposing changes to this.
Thanks, but I don't see @ahejlsberg mentioned anything about extending x?: T
to let
, var
or class members? anything on that as well? why isn't this discussed?
(Edit: I took it that by "property declaration" he meant "interface property declaration", not "class property declaration"?)
Just to reinforce the point made by @spion above, eliminating null
and using undefined
everywhere is _a workable strategy_ (i.e. enforceable), but the converse is not true: it's just not possible to eliminate undefined
.
In addition to the examples provided by @basarat five posts above, undefined
also occurs as a consequence of accessing elements outside the bounds of an array, or by invoking a function that does not have a return value.
Null can be eliminated by suitable encapsulation of the various sources that produce it. For example, for the DOM:
function getElementById(id: string){
const element = document.getElementById(id);
return element || undefined;
}
One would also implement a similar strategy for JSON received over the wire, which is the other main source of null
values.
Having said that, and while I believe the above is the best strategy, I do not really see how TypeScript can take one side or the other in the null vs undefined argument.
Consequently, if T?
means T|undefined
is not possible, rather than adding more syntax, my preference is for T?
means T|undefined|null
. One could then define a linting rule to discourage the use of null
.
@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 either undefined
or null
or both in a union.
However, you already have an shorthand expression in the language to denote T | undefined
:
interface MyInterface {
optionalValue?: string;
}
function doSomething(optionalValue?: string);
Have you considered simply extending it to let
, var
and class property expressions (or others that I might have missed)?
let optionalValue?: string;
var optionalValue?: string;
class MyClass {
optionalValue?: string;
}
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.:
Array<number?>
function myFunction(): number?
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:
interface SomeInterface {
optionalValue?: number;
}
class SomeClass implements SomeInterface {
optionalValue?: number;
}
@malibuzios We have indeed considered permitting the ?
name modifier in other places. In particular:
- On local variable names (e.g.
let x?: number
). - On property declarations in classes (e.g.
class Foo { x?: number }
). - On call signatures (e.g.
function val(x?: string)?: number
). - On index signatures (e.g.
[x: string]?: Entity
).
The effect would be to include undefined
in 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.
I'm glad you've considered it. I think it's an interesting direction, though on call and index signatures it does look a bit less "pretty".
In the case of index signatures: since a free-form object may return undefined
anyway when indexed with a non-existing key, it seems like it might be reasonable to extend the return type by | undefined
by default, but I guess this is not that obvious and open to discussion (the same argument can be applied to arrays, by the way - e.g. for out-of-range indexes, but I think it would be significantly weaker).
Example:
let someMysteryFreeformObject: { [key: string]: number };
someMysteryFreeformObject = callMysteryFunction();
let myValue = someMysteryFreeformObject["myKey"];
To be on the safe side, the type of myValue
could be inferred as number | undefined
by default. It may feel a bit unnecessary at times, but in some cases it could be helpful. Anyway, I'm not really sure at this point..
Edit:
Perhaps what I meant is that if the programmer truly intended myKey
to have a definite existence in the returned object, they could have simply added it to the contract, e.g.:
let somewhatLessMysteryFreeformObject: {
myKey: number;
[key: string]: number;
}
and then the compiler wouldn't infer myValue
as being possibly undefined
.
(This small divergence from the topic was mostly to show that the hypothetical { [x: string]?: Entity }
notation for index signatures may not actually be needed in practice)
I mentioned I thought applying this notation style to call signatures isn't really pretty:
function func()?: number {
...
}
But there might be some ways to mitigate that. One thing that comes to mind is what I'd call "implicit weakly defined return signature inference" (or something like that). What this means is that if the well-typed return value from within the function is detected to possibly be undefined
at one code path, the compiler would automatically add the | undefined
to the declared returned type of the function, instead of erroring, for example:
function func(): string {
if (getTimeOfDay() > 6)
return;
return "OK!";
}
let result = func();
The inferred type of result
here is not string
, but string | undefined
.
This "trick" wouldn't really help in interfaces or ambient declarations but I wanted to show there still are ways to avoid this less appealing syntax.
Anyway, there some other interesting advantages of the variable?: T
notation:
If T
is a union, say string | number
, it is more convenient and natural to write:
let val?: string | number;
than
let val: (string | number)?;
/// or
let val: string? | number?;
Another small thing, if some non-optional value is returned from a function and its type is inferred, this notation can easily "weaken" that inferred type to include undefined
as well, this way:
function func(): number {
...
}
let result? = func(); // The returned type is inferred as 'number' but the receiving
// variable's type is weakened to become 'number | undefined'
if (!resultIsValid(result))
result = undefined; // OK!
(I'm not sure how provably useful it is in practice but I thought it was somewhat "cool".. :) )
TL;DR: T? = T | null
is the right way to go.
This is going to be a though sell because you have chosen to explicitly forbid null
from 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 that T?
is possibly missing T
and they will use just that. None of them are going to say: "oh that's so a bad choice, I'm going to use type Opt<T> = T | null | undefined
". It is nice that you can do that, but the reality is that most programmers won't. And even if Opt<T>
is ok, it's nowhere as convenient as ?
for stuff that is going to be used a lot and sometimes composed (try Opt<Opt<number>[]>
).
I was happy to see @RyanCavanaugh's long comment above because I agree 100% with him. ๐
He explains clearly that in JS undefined
represents non-existent values: properties that are not defined, variables that are not initialized, missing keys in a Map.get()
, missing items in an Array.find()
, optional parameters, out of bounds array accesses and more.
This is not the same thing as null
which 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 is null
.
Using undefined
as your empty value is wrong: you can't make the difference between a missing optional parameter and a passed empty value.
// This:
function f(n?: number) {
// n is either a non-null number, or an optional parameter (undefined)
}
// Is very different from this
function f(n: number?) {
// n is a mandatory parameter, which might be null
}
// Which is very different from this
function f(n?: number?) {
// n might be an optional parameter, or it was passed with a possible null value.
}
You can't make the difference between a key that is not present in a Map
or a stored empty value:
let map: Map<string, number?>;
let x = map.get("key");
// x is undefined to indicate "key" is missing in the map.
// otherwise x is defined but might be null
JS is a dynamic language, if you assume dog: Dog?
property on objects, having null
means: "I don't have a dog". If your objects does not have a dog
property, you get the undefined
value, 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 null
you end up needing sentinel values... which actually are exactly as null
but 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 everything undefined
means 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 undefined
and null
is also interesting when debugging. If my program crashes on x.hello()
and x
is undefined
, I know that I have a different kind of problem vs if x
is null
. In the former it's probably because I used a codepath where x
was 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 get null
errors ever. On the other hand undefined
are 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 as T?
meaning T | null
. Optional parameters are implicitely undefined: f(x?: T)
is in fact T | undefined
. Optional members are implicitely undefined as well: { x?: T }
is in fact T | 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 see undefined
used is as the return type for functions that want to say "no result", for example:
Array<T>.find((T, number) => boolean): T | undefined
And to be honest in those very rare cases I would prefer typing T | undefined
rather than the cryptic T??
. Incidentally that also implies T?? = T | undefined
which 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.
What this means is that if the well-typed return value from within the function is detected to possibly be undefined at one code path, the compiler would automatically add the | undefined to the declared returned type of the function
Yup, it already works that way.
If my program crashes on x.hello() and x is undefined, I know that I have a different kind of problem vs if x is null.
Exactly. Because undefined
only exists for the purpose of being the value of things that don't exist or weren't set, you know that if you hit undefined
, that's what has happened.
If a developer uses undefined
instead of null
for things that are nullable, they won't be able to tell whether something is undefined
because it was explicitly set as such or because it wasn't set at all, and there will always be this strange thing called null
which doesn't fit into their mental model, which they have no use for, but they nonetheless encounter sometimes.
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:
If my program crashes on x.hello() and x is undefined, I know that I have
a different kind of problem vs if x is null.Exactly. Because undefined only exists for the purpose of being the value
of things that don't exist or weren't set, you know that if you hit
undefined, that's what has happened.If a developer uses undefined instead of null for things that are
nullable, they won't be able to tell whether something is undefined
because it was explicitly set as such or because it wasn't set at all, and
there will always be this strange thing called null which doesn't fit
into their mental model, which they have no use for, but they nonetheless
encounter sometimes.โ
Reply to this email directly or view it on GitHub
#7426 (comment)
.
although for undefined it's a bit different because the JavaScript runtime chose to use it for certain situations
It is precisely this which prohibits using undefined
for nullability, because it would give undefined
a dual/ambiguous meaning (property does not exist vs. property exists and is set to point to nothing).
function val(x?: string)?: number;
I believe this is not needed? I interpret the JS way is that you should check for undefined
and return null in those cases. Because you are requesting a certain value and null
means novalue
.
So people should use:
function val(x?: string): number?;
Otherwise I don't like the ambiguity that some people choose one way to do things and other people choose the other way of doing things.
@tinganho It's not necessarily needed, since a function should either return nothing (undefined
), or return something, which may be nullable (T?
). If you have a function that returns T
or T?
but it returned undefined
, that means you are missing a return ...
somewhere in the function body. (Again, it's important to distinguish between null
and undefined
here, because the occurrence of undefined
indicated a programming error.)
However, I would prefer ?:
be allowed for a return type regardless, purely for consistency, and it would read as "may return ...". It is surprising when type annotations that work in some places don't work in others. If ?: T
is just shorthand for : T | undefined
, you might as well allow it everywhere.
@jesseschalken I agree with you for consistency. Though, I'm afraid people will abuse it. The JS way is to require an additional check. And some people might be lazy and not write that additional check. If there is a way for a programming language to enforce a consistency it should utilize that opportunity.
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:
@tinganho https://github.com/tinganho It's not necessarily needed,
since a function should either return nothing (undefined), or return
something nullable (T?). If you have a function that returns T? but it
returned undefined, that means you are missing a return ... somewhere in
the function body. (Again, it's important to distinguish between null and
undefined here, because the occurrence of undefined indicated a
programming error.)However, I would prefer ?: be allowed for a return type regardless,
purely for consistency, and it would read as "may return ...". It is
surprising when type annotations that work in some places don't work in
others. If ?: T is just shorthand for : T | undefined, you might as well
allow it everywhere.โ
Reply to this email directly or view it on GitHub
#7426 (comment)
.
@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?
@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 for null
and undefined
can be written in shorthand:
Shorthand | Longhand | - |
---|---|---|
x: T |
x: T |
(existing) |
x?: T |
`x: T | undefined` |
x: T? |
`x: T | null` |
x?: T? |
`x: T | null |
For T | undefined
, the ?
against the identifier rather than the type is correct because "may be undefined
" means the same thing as "may not exist" and "may not be set". It reflects JavaScript's predefined meaning for undefined
as the value of things that don't exist or weren't set.
The only thing lacking is a shorthand for T | undefined
if you need it as a standalone type. But if undefined
and null
are being used correctly, you won't actually need it.
I wasn't sure if you were also referring to my comments as well, anyway I already mentioned that I am not opposed to the idea of T | null
having its own shorthand notation, but I wanted to concentrate on trying to "recycle" and extend the existing variable?: T
notation in the language, which has already been designated to mean T | undefined
, so could possibly be extended to places it is not supported today (namely variable declarations, class members, function return types and possibly index signature return types).
I feel that using T?
for T | null
may be technically possible, but less than ideal because it would create an ambiguity where the meaning of the question mark would change depending on its location, and it doesn't feel to me as a mark of a great syntax.
E.g. having:
let myVar?: number;
to mean myVar: number | undefined
, but
let myVar: number?;
to mean mayVar: number | null
Would become one of those places in the language where novices or people new or casual to it may tend to get confused because these semantics don't seem to feel intuitively obvious. I would personally prefer that a shorthand syntax for T | null
wouldn't get adopted in a haste but perhaps wait until some really great alternative notation is proposed for it.
(I've tried really hard myself to come up with an adequate alternative but I really couldn't find anything that I would feel as even worthy mentioning).
Would become one of those places in the language where novices or people new or casual to it may tend to get confused because these semantics don't seem to feel intuitively obvious.
A question mark after the identifier, x?: T
, means the identifier may not actually exist or be set, so reading it may produce undefined
.
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 produce null
.
It's pretty intuitive to me.
Well, I never said I thought it was a "complete disaster" :), but personally I feel it's just simply not good enough. Say it does get adopted, but then a couple of months from today somebody (perhaps even you or myself) randomly comes up with a better notation - that would be a shame there wouldn't really be any way to take it back.
The only remotely sane alternative I could come up with was using something roughly analogous to the "bottom" character like assigning the underscore (_
) to mean null
, then you could write something like:
let x: T|_
I just checked and surprisingly it turns out it is possible to even do it today because it seems like the underscore character, by itself, can be a valid type identifier, so one could define (I mean, when null
becomes a valid type):
type _ = null;
I'm aware it may not be of everyone's taste but that was the only thing I felt was remotely worth mentioning.
Say it does get adopted, but then a couple of months from today somebody (perhaps even you or myself) randomly comes up with a better notation - that would be a shame there wouldn't really be any way to take it back.
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 null
is a unit type, not a bottom type.)
Yes, I re-read the article I myself linked about the bottom type and realized the it isn't really what I initially thought so I edited my comment to state it was "something roughly analogous to".. the bottom type :).
Anyway, we all have differing opinions, and I can understand that some projects may also be tight on schedules (I speculate TypeScript 2.0 beta release is due for the Microsoft Build conference which I believe would start at the 30th this month). I said I feel that applying the ?
shorthand in differing positions with ambiguous semantics is not great, and although may actually feel intuitive to some, it may not feel intuitive to others. The only alternative I could imagine at this point was the T|_
notation, which although I'm not sure I'd actually use myself I still feel is more readable and intuitive to understand.
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
Again, you're confusing optionality and nullability. Take this for an example:
class Person {
public getCompany():Company? {
// ...
}
}
class Company {
// ...
}
/**
* Generate Report X for a person and their company (if any).
* @param p The person to generate the report for.
* @param c If specified, use this company instead of the person's company.
*/
function do_report(p:Person, c:Company? = p.getCompany()) {
// ...
}
var p1:Person;
var p2:Person;
// ...
// Do the report with p2's company instead of p1's.
do_report(p1, p2.getCompany());
If Company?
means Company | null
, the call to do_report()
will always use the company of p2
. This makes sense. The documentation says "If specified, use this company instead of the person's company." The parameter was specified, so it should be used. All is well.
But if Company?
means Company | undefined
, the call to do_report()
will only use the company of p2
if it has one, otherwise it will still use the company of p1
. This is not what the documentation says, but because undefined
was used to denote nullability, do_report()
can't distinguish between a nullable parameter that was passed and happened to be null, and one that wasn't passed at all. The undefined
has become ambiguous and the resulting confusion has caused a bug.
Also consider a container class which uses optional parameters of a generic type, as in foo(x?: T)
. If T
is filled with string
or string | null
, it will work fine, but if T
is filled with string | undefined
, it's not going to work at all, because the value undefined
for the optional parameter becomes ambiguous.
This is what I mean when I say the fact that the JavaScript runtime produces undefined
in certain situations gives undefined
a predefined meaning and thereby prohibits using it for the null side of a nullable type.
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
undefined value
primitive value used when a variable has not been assigned a value
null value
primitive value that represents the intentional absence of any object val
Of course you can do what you want in your code but this is the intended usage of undefined
and null
as prescribed by JS itself. I feel like TS should support any style of code, but encourage idiomatic JS.
Response to criticism of T? = T | null
From the TS design meeting notes #7488
People will definitely be confused about which to do.
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"
and x == 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 -- means T | null
and param?
to be an optional parameter/member, which can be undefined
... 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.
Lack of symmetry between optional members and nullish types
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 between x?: number
and x: number?
because they are not the same thing at all!
This is no more confusing than the upcoming this
typing, where x(): number
and x: () => number
are 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 undefined
or its difference with null
and 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 null
and undefined
types 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 | null
and 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:
- Having a set of hieroglyphs
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)! - Trying to extend the
?
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
- We don't introduce shorthands like
T?
. Every style is supported the same way but none is great. ๐ฆ - We support every style at once with
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 haveT
notT | undefined
(given it was declaredx: T?
). ๐ฆ - We don't please everyone. We choose what we think future code should look like (whatever that is) and add built-ins helpers for others. Like add
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. - We make the meaning of
T?
definable intsconfig.json
+ in the.d.ts
file pragma. I don't think it's elegant, but I think it's the only sane way to please everyone.
Other ideas?
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 null
or undefined
is a very good thing, rather than lots of people assuming that T?
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?: string
in those positions to string|undefined
. Additionally, the "possibly-uninitialized" variable syntax (let x?: string
) will be supported, also implying T | 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
, or T | null
appearing in well-formed definition files, we can certainly revisit the topic and see if it makes sense to introduce a T?
shortcut.
Using undefined
for 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?
for x: T | null
, but preserving x?: T
for x: 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.
@RyanCavanaugh Glad to see this moving forward! Not delaying because of the lack of consensus on T?
is a great choice.
Maybe we'll find out that nullable variables shouldn't be the norm and Nil<T>
or T | null
(substitute undefined
if you want) don't actually require shortcuts!
I am sorry to say but let x?: string
seems like a mistake to me. I suggest that you take the same stance as for T?
and wait a bit.
f(x?: string)
is a parameter you can omit when calling f()
.
It's different from f(x: string | undefined)
, which you can't call without a parameter f(undefined)
.
{ x?: string }
is a field you can omit, e.g. when creating options objects: { }
.
It's different from { x: string | undefined }
, which you can't omit: { x: undefined }
.
let x?: string
is what? It's a variable that was declared and that's all. ?
does not apply to the x
identifer but to its type... It's nothing more than a way to say let x: string | undefined
.
Do you see the difference? You made the point that let x?: number
and let x: number?
would not be symetrical and people would have a hard time deciding which one to use. By introducing this notation that is devoid of any meaning with respect to x
, you are setting yourself up for a confusing language design that will impact negatively future choices, like a possible T? = T | null
.
Also, from outside it seems a bit like you're giving yourself the T? = T | undefined
notation under a disguised form, maybe because TS code base is written in this style?
If you don't want to create a shortcut for nullable types yet, you shouldn't be introducing another one that is just that.
@jesseschalken We can always add T?
later, but we can't take away x?: T
. I should add that we're also planning to introduce two type aliases in lib.d.ts
:
Nullable<T> = T | null;
Optional<T> = T | undefined;