News for hackers. Stuff that matters.
Re:Bad TypeScript Habits by recursive on Tuesday February 02, 2021 @05:55PM (#26008181)
Disagree on #10. The amount of code I've seen that intentionally distinguishes between null and undefined is... well, zero. On the other hand, the amount of code I've seen that unintentionally distinguishes between them when it shouldn't have is not zero.
Re:Bad TypeScript Habits by throwoutttt on Tuesday February 02, 2021 @06:21PM (#26008474)
Undefined and null should be treated differently.

null: the value exists but is not set.

undefined: the value does not exist.

JSON.stringify on an undefined property removes that property.

JSON.stringify on null keeps that property as null.

A codebase that does not distinguish between null and undefined is a less flexible codebase.

Re:Bad TypeScript Habits by bikeshaving on Tuesday February 02, 2021 @06:35PM (#26008605)
Trying to finely parse the semantics of null and undefined like this is a fool’s errand, insofar as there isn’t much in the language which enforces your conventions. Also, these conventions fall apart when we check how null is actually used. For instance, all DOM nodes have a default of null for their first children and next siblings. Does this mean “the value exists but is not set?” What does it mean when the browser is setting the value and not the user?

At the end of the day it would have been much nicer for there to only have been one nothing type but what’s done is done.

Re:Bad TypeScript Habits by chrismorgan on Tuesday February 02, 2021 @09:00PM (#26009725)
Element.foo being undefined says “foo? What’s foo? I don’t know what you’re talking about.”

Element.{firstChild, nextSibling} being null means “I know what firstChild (or nextSibling) is, but this element doesn’t have one.”

It would be categorically wrong for things like Element.{firstChild, nextSibling} to be undefined.

Re:Bad TypeScript Habits by bikeshaving on Tuesday February 02, 2021 @09:23PM (#26009839)
Some developers would interpret your rule as being, if it doesn’t make sense for you to make this property access on this specific object, the value should resolve to undefined and not null. Problem is, there already are mechanics for a kind of check like you describe (the in operator and hasOwnProperty both kinda do what you’re looking for). Moreover, if you were to design the DOM API from scratch, by your logic some developers might assume that text nodes, which never have children, should have a firstChild set to undefined and not null, because it should say “I don’t know what you’re talking about.” You can debate this last point, but know that some people are going to interpret the null/undefined rules as you describe in this way.

I’ve seen null/undefined semantics play out; they more or less collapse under the slightest of deadline pressures or scaling of team sizes. There are infinite ways to splice the idea of “emptiness” or “lack of data” and any definition you provide (the definitions both you and OP provide give a lot of wiggle room) would be interpreted fifteen ways by fifteen developers, so much so that even changing hands just once will cause any null/undefined distinctions to sublimate in any codebase.

There are so many languages which do just fine with one null type; I don’t understand why JavaScript developers have to do these sorts of mental kinesthetics to justify the existence of two in JavaScript, especially given the more or less insane fact that `typeof null === "object"`. If you consistently use undefined or null exclusively internally, and accept both via the == null trick externally, you can pretty much avoid having to deal with null/undefined semantics entirely.

Re:Bad TypeScript Habits by chrismorgan on Wednesday February 03, 2021 @01:59AM (#26011299)
The early DOM APIs certainly have some warts due to inheritance design mismatch, because they designed things in a single-inheritance way despite DOM actually being multiple-inheritancey. These days, new things like the append() method get added to the ParentNode interface, so that they’re provided by Element and DocumentFragment, but not such as Text or Comment; but back when they designed these things at the start, they only had Node, and decided to implement these methods on Node rather than individually on Element and DocumentFragment. While I imagine there were practical reasons they did it that way then, with the benefit of hindsight I state categorically that this was a mistake and is a design error. If the spec- and browser-makers were willing to break backwards compatibility, firstChild would certainly be shifted from Node to ParentNode, and text nodes would no longer have a firstChild member.

> There are so many languages which do just fine with one null type; […] two in JavaScript

I quibble over your claim here, because JavaScript doesn’t have two null types. undefined is not a null type. Rather, it’s JavaScript’s equivalent of what is in most dynamically-typed languages an exception, and a compilation error in most or all statically-typed languages. As a concrete example, where JavaScript produces undefined, Python raises AttributeError or KeyError. There are similarities to NaN as well—both come from a philosophy of trying to keep the code running even if you ask for something that isn’t defined. Yes, undefined then behaves a lot like null in some ways, just as NaN behaves like a number in some ways (though the two cases are not parallel, merely passingly similar). But then, false behaves like null in most of the same ways too (though not quite all—most notably, the `== null` comparison—but it’s well understood that a lot of JavaScript’s == comparisons are illogical and inconsistent, so this should not be considered significant to the design); yet no one claims false to be a third null value.

Re:Bad TypeScript Habits by Jasper_ on Tuesday February 02, 2021 @09:58PM (#26010068)
Why does Array.prototype.find() return undefined and not null if it didn't find a value? According to your logic, the sentinel to indicate "this array doesn't have an element that matches the predicate" should be null.
Re:Bad TypeScript Habits by chrismorgan on Wednesday February 03, 2021 @02:07AM (#26011327)
I don’t know. I would guess that that return type involved a slightly heated discussion before undefined won over null so that you can distinguish it returning an element that is null.

(This is a fundamental problem of nullable types which is solved by algebraic data types; in Rust, the equivalent method would return None for “no value found”, or Some(None) if it found a None value.)

Re:Bad TypeScript Habits by WorldMaker on Wednesday February 03, 2021 @01:41PM (#26017364)
Also, the "wisdom of the Ancients" in JS was to never intentionally set a value to `undefined`. While a lot of the quirks have been paved over by TC39, `undefined` was originally considered an implementation detail of the JS engine and had different semantics in different engines, especially when trying to set something to `undefined` (`myobject.field = undefined` might be equivalent to `delete myobject.field` in one engine, equivalent to setting to `null` in another engine, and its own explicit primitive value in a third, while a fourth threw an error when setting anything explicitly to undefined because it was not a value at all). Even with paved over semantics and a general convergence among JS engines (and a near monopoly of V8 in practical usage), I still find it worrisome seeing codebases that treat undefined and null as very distinct values instead of shades of gray of the same concept, because `undefined` certainly wasn't meant to be a value originally.
Re:Bad TypeScript Habits by recursive on Wednesday February 03, 2021 @04:54PM (#26019670)
There has been no version of javascript where it was practical to avoid setting a variable to null. As far as I know, things like these would always do it.

    var x = {}.foo;
    var y = (function() {})();
These are contrived examples, but they represent things done by very reasonable code. There are all kinds of ways that `undefined` can get assigned into a variable. It's inevitable that those cases would have to be handled.
Re:Bad TypeScript Habits by WorldMaker on Wednesday February 03, 2021 @07:51PM (#26021257)
I'm saying it impractical to consider `null` and `undefined` as different "values" in JS. `undefined` was originally built to be a "thrown" exception built for a language without thrown exceptions. It wasn't intended to be a placeholder value like null is. The language today makes it possible, you can today write `var x = undefined` and expect things not to blow up or behave all that differently/quirkily between browsers and browser modes. I'm still going to be worried/skeptical of any code that intentionally uses patterns like that of setting objects and properties explicitly to `undefined` and treats that as a different value from `null`.

Yes, you have to handle cases where things are undefined, but I'd be wary of handling them too dissimilarly to where things are null, because `undefined` was not designed to be "Null 2: Electric Boogaloo", it was designed to be "404 Item Not Found Exception" in the time before JS had real exceptions.

Re:Bad TypeScript Habits by cjonas on Wednesday February 03, 2021 @12:25AM (#26010873)
A better example is querying from a database or api where you can specify the returned properties. null === queried but no value vs undefined === not queried
Re:Bad TypeScript Habits by Jasper_ on Tuesday February 02, 2021 @09:38PM (#26009933)
> JSON.stringify on an undefined property removes that property.

Except, you can still differentiate between them:

    > ({ foo: undefined }).hasOwnProperty('foo')
    true
    > ({ }).hasOwnProperty('foo')
    false
So now you have two ways of specifying a missing property, instead of one. null vs. undefined is a really annoying language misfeature to me, especially with some of the other weird behaviors that it has:

    > [].length
    0
    > [].map(v => 123)
    []
    > [].map(v => 123).length
    []
    > [undefined].length
    1
    > [undefined].map(v => 123)
    [123]
    > Array(1).length
    1
    > Array(1).map(v => 123)
    []
    > Array(1).map(v => 123).length
    1
If you need to remove a property from an object, just use delete obj.foo; instead of setting it to undefined.
Re:Bad TypeScript Habits by WA on Wednesday February 03, 2021 @03:25AM (#26011699)

    JSON.parse(JSON.stringify(obj)).hasOwnProperty('foo')
    false
Thing is, other parts of the code might actually serialize and unserialize objects. If you rely on a prop being 'undefined' meaning "prop is unknown like the firstName", as OP suggested, you might be in for a surprise.
Re:Bad TypeScript Habits by ledauphin on Tuesday February 02, 2021 @09:16PM (#26009801)
a codebase that uses static typing _in the first place_ is a less flexible codebase.

"Less flexible" != "Less correct" or even "less good".

It's true that these are subtly different concepts. It's arguable whether they are helpful distinctions for the long-term health of a codebase.

Re:Bad TypeScript Habits by dvdhnt on Tuesday February 02, 2021 @06:06PM (#26008305)
Hmm, not sure what you're saying exactly. Null and undefined are different. A common example would be any sort of patch operation - setting a value to null is valid, whereas if the value of that property is undefined, it likely should be ignored.

Sorry if I am misunderstanding your statement.

Re:Bad TypeScript Habits by srcreigh on Tuesday February 02, 2021 @06:58PM (#26008813)
A concrete example of this is setState in React.

T | null signifies that it can be cleared by using setState({prop: null}).

T | undefined means that it may start out undefined, but if it becomes defined it'll probably stay that way. If you try doing setState({prop: undefined}), it won't work.

Re:Bad TypeScript Habits by recursive on Tuesday February 02, 2021 @08:17PM (#26009449)
There doesn't seem to be any fundamental reason why that had to be the case. I'm not a react expert, but it seems like it would have been more obvious to me if null and undefined were treated the same. The distinction should be whether the key exists in the object.
Re:Bad TypeScript Habits by presentation on Tuesday February 02, 2021 @08:49PM (#26009658)
It's a poor design carried over from legacy JavaScript, but there are clear differences at the language level. While not the only one, the biggest one to me is this kind of code:

const x = {};

x.foo // returns undefined

const y = { foo: undefined };

y.foo // also undefined

Basically a member not being provided will return undefined, whereas `null` had to actually be set. Hence I think the tendency for checking for the absence of something via `undefined`, while expressing the deletion of something with `null`. Sure, you can assert more specific types in TypeScript to step around that, or you can do something less intuitive like checking for `keyof` to detect absence; but this is both straightforward and presents less issues when interfacing with inputs from non-typescript-using users.

Re:Bad TypeScript Habits by rfw300 on Wednesday February 03, 2021 @03:32AM (#26011746)
I think what he’s saying is not that there is no semantic difference between the two, but that in practice programmers use the two so interchangeably that not checking for both is likely to cause more trouble than it’s worth.
Re:Bad TypeScript Habits by lopatin on Tuesday February 02, 2021 @06:09PM (#26008328)
Yes plz. No one wants to know the difference between those two. Of course, you still do need to. But don't make the problem worse by writing code breaks when you mistake one for the other.
Re:Bad TypeScript Habits by SomeCallMeTim on Tuesday February 02, 2021 @07:44PM (#26009204)
Totally agree with you.

Some of the rebuttals below seem to be saying "But I use them differently!" Bully for them, but 99% of the time when it's not set, you don't really care why it's not set because it's not defined yet or because it's been set to `null`.

And any time they assume it can only be null OR undefined but not either, there's a chance for a bug. Even if I "know" it can only ever be null and not undefined, it's safer to just say != null rather than !== null.

Treating them differently in a code base is a legit semantic, but testing with != null works no matter the intended semantics.

Re:Bad TypeScript Habits by Normal_gaussian on Tuesday February 02, 2021 @06:12PM (#26008355)
Welcome to any of my codebases. Undefined and null have clear semantic differences, and those differences are important to the function, safety, and performance of the code.
Re:Bad TypeScript Habits by patrickthebold on Tuesday February 02, 2021 @07:19PM (#26009003)
Curious: Roughly, how many people work on those code bases?
Re:Bad TypeScript Habits by recursive on Tuesday February 02, 2021 @08:15PM (#26009427)
That's perfectly fine. If you need to distinguish between them, then you should do that. There's plenty of code that's different though. I would say probably most of it.

The semantic differences seem to be local at best. In any case, there's plenty of code that legitimately doesn't concern itself with any of that.

Re:Bad TypeScript Habits by spinningarrow on Tuesday February 02, 2021 @06:15PM (#26008404)
An example of where the distinction is quite important - default parameters for functions only work when the argument is undefined.
Re:Bad TypeScript Habits by recursive on Tuesday February 02, 2021 @08:09PM (#26009393)
What do you mean by "work"? If you assign a default parameter value, you never need to mention undefined or null.
Re:Bad TypeScript Habits by BigJono on Wednesday February 03, 2021 @02:44AM (#26011493)
'work' as in if you explicitly pass in undefined as a parameter to a function, the default parameter will be used in it's stead.

I've never ever run into this because I never use undefined for anything other than stuff that's explicitly not set, and I have no fucking idea why other people are. But I can definitely see it causing bugs if you do stuff like use '!=' everywhere and treat null and undefined as the same thing.

This is why the original comment is wrong. You should be narrowing your types where possible. It costs nothing to just always use null if you're explicitly setting something, then you can use '!==' wherever you want, which means that if someone else sets something to undefined you'll catch the bug straight away when it's easiest to catch.

If you treat them the same you're just asking for some 3rd party lib to do a '!== null' or accidentally overwrite a "null" with a default parameter you're not expecting, and that's way, way harder to troubleshoot.

Re:Bad TypeScript Habits by recursive on Wednesday February 03, 2021 @10:38AM (#26014563)
I've never run into this, because I never explicitly pass an expression when I want the default parameter value to be used.

You could just as easily run into a bug with 3rd party lib code by distinguishing between the two when that library doesn't.

The only reason I've ever intentionally used `null` in my own code is that it precedes `undefined` in the default array sort. But if I ever found myself using library code that required `null` for something, I'd say "Huh. That's weird." And then I'd pass it a `null`.

Re:Bad TypeScript Habits by BigJono on Wednesday February 03, 2021 @11:26PM (#26022573)
> I've never run into this, because I never explicitly pass an expression when I want the default parameter value to be used.

What? That's not what prevents you from running into bugs. The bugs come when you treat null and undefined as equivalent and try to pass them into a function with default params, without knowing which one you have. Because the defined behaviour of default params is different for null and undefined.

For example, say you have a data type of 'null or [0...11]' for a month input the user may or may not have interacted with yet. It gets passed around a bit and ends up going through a function defined with default params, like 'function doSomethingWithDate(day = 0, month = 0, year = 1900)', and in that function there's a null check like 'if day, month or year is null, do whatever we want to do if the user hasn't entered a date yet'.

A few months later someone picks up a ticket to refactor the date input, and they use 'undefined or [0...11]' for the month instead, which doesn't matter because your team always just checks both with things like '==', '!=' or '??'.

Except surprise, it does matter, because now something somewhere else breaks because it thinks the state is 'the user has entered 1/1/1900' instead of 'the user hasn't entered anything yet'.

Of course you might not run into this specific bug, because the code is a bit contrived, and most good programmers will recognise that whatever 'doSomethingWithDate' is it probably doesn't need to be run unless the user has actually entered a date already. That null check should be happening as close to the user input as possible, and there shouldn't really be any room for people to use default params between those two places.

The problem is that in the real world people write dogshit code like this all the fucking time. That would rate at about the 5th percentile of all the garbage code I've seen in my career.

My point is that that's literally impossible to run afoul of language differences between null/undefined if you're strictly using either null or undefined, as opposed to this blase mindset of "just always check for either", because you now know what your data type is while you're writing the code, and can look up the standard library or 3rd party library docs to see how they interpret it. If you have data types of 'null or undefined or ...' then you don't know which one you've got until runtime and you need to account for both possibilities everywhere. That means coercing it into either specifically null or undefined every single time you want to pass it to a library function, your own codebase's functions that use default params, or any other place the language itself treats undefined and null in different ways.

Always use the minimal data type for representing whatever you need. If you don't then you're literally choosing to have ambiguity in your code base. It makes no sense.

Also this:

> You could just as easily run into a bug with 3rd party lib code by distinguishing between the two when that library doesn't.

...is nonsense. If the library doesn't distinguish between the two then by definition it cannot matter which one you pass in, since it's treating them the same.

Re:Bad TypeScript Habits by WA on Wednesday February 03, 2021 @03:31AM (#26011739)
Yes, I disagree on #10 as well. Because: createNewMessagesResponse(null) 'You have null new messages'

Of course, TypeScript will say that 'null' can't be passed to createNewMessagesResponse directly. However, data gets pushed around a code base a lot and might go through third party packages or come in from an API response and could very well be 'null'. I proactively don't check for 'undefined', because I can't really trust this to be 'undefined'. Furthermore, in the example OP provided, 'null' and 'undefined' should have the same result (no message is shown, because no number was passed to the function).

Re:Bad TypeScript Habits by SkyPuncher on Tuesday February 02, 2021 @10:32PM (#26010311)
IMO, the last 3 should really just be "be explicit when it matters".

I see more room for error with being explicit as a default than generalizing as a default.