1. 12
  1.  

  2. 12

    Ruby has had this for a little while, and I accept that this is a popular language feature… HOWEVER: the more I see this sprinkled around code, the worse of a code smell I find it to be. I have actually seen a style emerge where people default to using this rather than traditional method-calling, and it drastically increases the complexity of a piece of code. It’s almost never tested behavior, it’s more of a way to avoid failing-fast, ensuring that tracking down the true source of a problem is much, much harder.

    Like any tool it can be useful and it can also be misused - I have to accept that it’s here to stay in null-permitting languages. For now it bums me out, though.

    1. 3

      Yes. I think it should only be used at the boundaries as part of quick and dirty validation. If you’re using it outside of a constructor/converter, you’re using it wrong.

      1. 3

        Speaking from TypeScript, in which you can constrain away nulls at any declaration:

        I think it’s a matter of the data you’re dealing with. If a data type has optional properties, it should be fair game for use with optional chaining. Getting an optional result from a property lookup means you can collapse multiple null checks into one and get code that’s easier to read, for instance.

        But first, the data type has to be suitable. Most functions’ argument types ought to disallow undefined / null, if what the function would do in that case is return undefined / null anyway. Push the responsibility for that check back to the caller, and it will both reduce the function’s complexity and prevent calling it with optional-chained expressions. “For best results, squeeze tube from bottom.”

        1. 2

          Clojure has a threading macro which allows this, and doesn’t turn into a code smell but a warning that there will be special casing for nil and the check for a valid value is in that section. Normally the behavior it guards is tested or relied on elsewhere, and not just a throwaway “don’t crash here please.”

          1. 2

            In Ruby code at work, we have a strict rule against chaining the safe navigation operator.
            Single use is discouraged, but allowed in situations like simple presentational code, or if used in a way that does not propagate null values further, e.g. when the code provides a default:

            user.favorite_color&.titleize || "None"
            

            In cases like this it is being used a slightly-more-terse replacement for a ternary operator:

            user.favorite_color.present? ? user.favorite_color.titleize : "None"
            

            I don’t care for it myself, but enough of the team does, so the compromise has been allowing these singular uses, but never chaining.

            I agree with you. Chaining is a big red flag. If one is drilling down into some crazy deep object chain as in user&.company&.address&.state, it is a sign that something else is wrong, and the need to reach for the safe navigation operator is a smell pointing to that.

          2. 8

            This isn’t a Typescript thing, it’s in regular JavaScript https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining (for those who only read the title)

            1. 5

              I’m also a little surprised at the popularity of this article. Not only is this not really a TypeScript feature per se, but TypeScript support for it has been out for almost two years now. Anyone writing TypeScript in VS Code has probably seen it auto-insert option chaining into statements with nullish properties. This auto-insertion is actually frustrating for those of us free from the shackles of IE 11 and Babel but still stuck with Webpack 4 since the parser it uses doesn’t support this or other recent syntax innovations.

              1. 1

                How has it come about that you are able to use modern tsc while simultaneously being stuck on old webpack?

                1. 2

                  Stuck on Vue 2 like much of the Vue ecosystem, at least at the time we started our project.

                2. 1

                  I used it as a jumping-off point to talk about optional chaining in general TBH

              2. 1

                But why now?

                Why ignore the problem for 25 years, then turn around and admitting the problem when null and undefined has been a problem for longer than most of us have lived?

                1. 22

                  Because we are living through a programming language renaissance: it seems that major languages (Java, JavaScript, C++, Python) had a period of stability/stagnation, when they were considered “done”, as they fully implemented their vision of OOP. Then, the OOP craze subsided, and people started to look into adapting more of FP ideas. Hence, all those languages finally started to get basic conveniences as:

                  • statement-level type inference (auto in C++, var in Java) / gradual types (TypeScript, mypy)
                  • optional and result monads (?. in JS, optional/outcome in C++, optional in Java. Curiously, Python seems to get by without them)
                  • data types (records in Java, <=> operator/hash in C++, NamedTupple/@dataclass in Python. Curiously, JS seem to get by without them)
                  • sum types / union types / pattern matching (pattern matching in Java, variant in C++, pattern matching in Python, union types in TS)
                  • destructing declaration ( auto [x, y] = in C++, richer unpacking syntax in Python, let { foo } in TypeScript. Nothing yet in Java I think?)
                  • async/await (curiously, Java seems to have chosen stackful coroutines instead)
                  • and, of course, lambdas. JS doesn’t get enough credited for being the first mainstream language with closure functions, but they shortened the syntax. Python curiously got stuck in local optima, where lambda was pretty innovative at the time, but now feels restrictive.
                  1. 1

                    data types… Curiously, JS seem to get by without them

                    FWIW the way anonymous objects (and their types in typescript) work in JS is just as convenient for casually structuring data as e.g. NamedTuple in Python.

                    1. 1

                      I consider structural eq/hash/ord a part of being a data type. I think JS still doset have those?

                      1. 1

                        No and it’s never getting them, but oh well, eq is in lodash.

                  2. 3

                    The “billion dollar problem” hasn’t been ignored, modern languages (last 5-10 years), usually treat null and undefined completely different than the past. Nowadays, they are treated as algebraic data types (like tagged unions) that must explicitly be checked by the developer, avoiding runtime NullReferenceExceptions.

                    New syntax ,such as ?., tries to make these checked as easy as possible.

                    //c, this value may or may not be null
                    SomeStruct * s = get_struct();
                    //This may or may not be an error
                    int value = s->some_field;
                    
                    //Typescript
                    type ReturnValue = SomeStruct | null
                    const s : ReturnValue = get_struct();
                    //The compiler will not allow this
                    Const value = a.some_field;
                    //you have to do this
                    const value : number | null = s !== null ? s.some_field : null;
                    //or with the new syntax
                    const value  = s?.some_field;
                    
                    

                    Where it really shines, is chaining multiple checks together:

                    //Turn this:
                    Interface I {
                      field1?: {
                        field2?: {
                        field3?: number
                      }
                      }
                    }
                    function func(arg: I | undefined){
                      If(!arg){
                       return;
                     }
                     if(!arg.field1){
                        return;
                     }
                     if(!arg.field1.field2){
                        return;
                     }
                     return arg1.field1.field2.field3;
                    }
                    
                    //into this:
                    const func = (arg: I | null) => arg?.field1?.field2?.field3;
                    
                    1. 1

                      Er, that’s my point. The problem has been known for fifty years, solutions have been known for a long time as well. Why did TS/JS ignore the problem until now?

                      1. 5

                        Because it used to have much bigger problems to deal with first.

                        1. 3

                          Just because someone knows the problem doesn’t mean it’s in everybody’s understanding, or on everybody’s roadmap.

                          1. 1

                            The problem was solved in a few languages for a long time (e.g. Haskell) - it’s just popularity.

                        2. 2

                          Honestly, having null and undefined doesn’t bother me at all, as long as they are part of the type system- as is the case in TypeScript.

                          They mean subtly different things. The most common convention is that null means “this value has been explicitly set to empty/bottom/default” and undefined means “this value has not been set”. (I know this is not the case for the TypeScript compiler project that just prefers undefined for everything)

                          It would be better to just have an Option<T> type, IMO, but TypeScript is trying to be a thin veneer over JavaScript, so that’s out of scope for it.

                          1. 2

                            In my opinion, null and undefined actually conflate three different cases:

                            1. This variable has not been defined
                            2. This variable has not been given a value
                            3. This variable has no value (or value not found)
                            1. 3

                              Interesting. If I understand your list, #1 refers to a variable name just not existing at all in scope (never declared with var, let, const), #2 refers to a let or var declaration, but no assignment: let foo;, and #3 is setting something to null.

                              I think you’re right and that makes sense, but do you see any value in differentiating scenarios 1 and 2?

                              Also, if I understand and remember correctly, #1 is not allowed in TypeScript at all. So I think, for TypeScript, your list and my described convention is the same.

                              1. 1

                                For languages where you do not have to pre-declare variables, then yes, there is a value in differentiating between 1 and 2.

                        3. 1

                          I think it’s weird that in TypeScript I can’t do foo?.bar for any bar on any object. Let me explain with an example.

                          function doStuff(x: unknown) {
                              const field = x?.field ?? 0
                              // do some stuff
                          }
                          

                          Why should the compiler bitch at me about this? I know x is unknown, but it might have field as a field. I access it with the optional operator, so it should be fine. This isn’t just unknown, either- you’ll get the same error with other types that aren’t guaranteed to have the field, such as an interface or object.

                          Sure… you probably shouldn’t be doing that often, but it’s not wrong, IMO.

                          1. 3

                            If typescript allowed accesses to properties that aren’t defined in the type, you’d lose the ability to catch typos in field names.

                            The unknown type is IMO a little flawed in that tsc makes it a little harder than it ought to be to narrow it down to anything else with guards.

                            1. 1

                              That’s a great point. Thank you. I can’t believe that I hadn’t considered that.

                              I was just frustrated that the mental model of JavaScript objects basically being dictionaries doesn’t really translate to TypeScript.

                              I do still think (and agree with your last statement) that it should be easier to work with unknown objects. It’s a pain in the butt to have to type if ("foo" in x) { x.foo }, especially because TypeScript, like JavaScript, is all statement-based, so if isn’t an expression. Just makes everything more tedious.

                              1. 1

                                When I’m working with unknown in practice, I almost always define guard functions to get around the holes. Obviously they introduce more problems if they happen to be buggy but, ah well.

                                function isObject(obj: unknown): obj is object {
                                  return typeof obj === "object" && obj !== null;
                                }
                                

                                Ternaries are expression-level, so "foo" in obj ? x.foo : null would work? Type narrowing propagates from the left hand sides of x?y:z, x||y and x&&y to the right hand sides .

                                1. 1

                                  Does your guard function actually have to check for null? If the type of obj is “object”, can it possibly be null? What about undefined (which unknown could also be)?

                                  IIRC, object type doesn’t make my situation better, either. I think it’s also “opaque” to dot-notation field access. I think I just need to define a simple function for these places, but it’s kind of a bummer that I have to do that. Not a huge deal, just a “papercut” kind of thing.

                                  1. 1

                                    typeof null === "object" is one of the famous stupid-looking things in JavaScript that probably can’t be changed ever.

                                    typeof undefined === "undefined" though, so that one’s fine.

                                    Yeah object doesn’t get you much (and I can’t remember if I meant to use Object) but it’s much less hostile than unknown for further narrowings.

                                    A nice thing I’ve tried at work recently is using a library like yup to check large objects all in one go, like:

                                    interface Foo {
                                      x: number;
                                      y: number;
                                      name: string;
                                    }
                                    const fooSchema = yup().object({
                                      x: yup.number.required(),
                                      y: yup.number.required(),
                                      name: yup.string.required(),
                                    }).noUnknown(true).strict(true);
                                    const isFoo = (obj: unknown): obj is Foo => fooSchema.isValidSync(obj);
                                    

                                    Transcribing the TS type manually into yup is a little boring but easy enough.

                            2. 1

                              In a user typed system, any object without a type must be opaque. Otherwise you’d have a type for “might have field.”

                              1. 1

                                I think you want any, not unknown. Then it will compile.

                                1. 1

                                  Not quite. any is too permissive. That would allow me to just assume the field is definitely there. any more-or-less turns off all of the type checking of TypeScript, which isn’t quite what I want. I want a safe way to say “give me this field if the object has it, or give me undefined if it doesn’t” without a verbose if-statement to check first.

                                  1. 1

                                    That’s what it does (check the compiled output). If you want any deeper checking, you probably have to use types, since it is the types that typescript is checking. :)

                                2. 1

                                  Sorry, couldn’t you just do x.field || 0? This works in JavaScript, not sure if it does in Typescript.

                                  1. 1

                                    Well, I have all of the strict compiler settings turned on, so I’m not 100% positive if that’s ever allowed in TypeScript, but for me it isn’t allowed. You cannot use dot-notation on an unknown type until you cast it (or “type narrow” it) to something else.