TypeScript: Guidelines for choosing between `null` and `undefined` with `strictNullChecks`

Whenever you want to make a type optional you have to choose what value to use for the missing values:

  • undefined: Has the benefit that is already there when you don’t write anything.
    • lib.d.ts contains 12 references of | undefined.
  • null: Familiarity, better interaction with REST Services (undefined disappears).
    • lib.d.ts contains 473 times of | null.*
  • undefined | null: Is too long, I was surprised that the type expression T? is not introduced as equivalent to T | null | undefined. BTW this example is misleading.

In my opinion for most of the cases the distinction is useless as long as you use non-strict comparison (==. =!) and boolean operators ( && ||).

Unfortunately writing | null | undefined is way too cumbersome, specially if you need have more than one optional thing in a type expression.

This is a real case:

Promise<Array<T | null | undefined> | null | undefined> | null | undefined

Some recomendations?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 20 (12 by maintainers)

Most upvoted comments

One thing to note is that “optional” is really T | undefined ; this is how the JS values will be initialized at runtime. and optional or undefined parameter, property or variable is initialized to undefined, and not to null.

null is a sentinel value that is used to signal the lake of a value. you have to set that up yourself, and it is not done to you by the engine.

Without going into ideological arguments, might be worth watching Douglas Crockford’s video when he talks about this in details; the main issue is most programming language use one value to indicate what undefined and null are used for. so in a sense JS has one too many notion of nothing-ness.

The TS compiler internally opted to use only undefined. Part of that is we do not have dependencies on other libraries, so we can be as prescriptive as we want. Some APIs, e.g. DOM, will return T | null consistently, so there is no way around undefined. So i would say it is up to you and your dependencies.

If you want to use both null and undefined i would recommend adding a new type alias: type Maybe<T> = T | null | undefined; and using that everywhere.

If indexers contained undefined every array access a[i] would be T | undefined. This is very annoying and would be borderline unusable.

One thing to note is that “optional” is really T | undefined ;

For future readers, this isn’t quite true. You can’t define an interface with a property T | undefined if you want that property optional. Instead, you must use the ?: syntax to have an optional interface property: interface I { name?: T } Using interface { name: T | undefined } will force you to explicitly assign undefined to property name, but it will not allow you to leave it out. Unfortunately, this means that there are really 3 types of “no value” in TypeScript (?:, undefined, null).

@aluanhaddad Sorry I wasn’t more clear, that is what I meant . Will edit my comment.

I know, I’ve been very active in this thread and in the previous one in Codeplex, but it has been delayed for C# 8 :S

If you were indexing by the wrong key, adding the ! doesn’t make you index by the right key. If you indexing by the right key, adding the ! is just an annoyance. All that happens in practice is that the syntax for indexing is x[k]! instead of x[k] with no added type safety.

Well, ! is just a shortcut for a casting to the non-nullable version of the type.

As any casting the compiler asks the developer to pay special attention to a potentially unsafe action, and in exchange the compiler trust the developer criteria.

Using an indexer is unsafe, because independently of the declared return type, potentially you’ll get back some undefined values.

Example:

const cache :{ [v: number] : number} = {};
function fib(v : number){
    if(v <= 1) return v;

    const c = cache[v];
    if(c != undefined)
        return c;

    return cache[v] = fib(v-1) + fib(v-2)
}

I think it’s obvious that the type of c should be number | undefined from a formal point of view.

Also the undefined gets introduced in the indexer action, not the data structure itself, because Object.values(cache) should be of type number[].

Something different is that this would be annoying in many other practical examples, but I suppose I have now a thicker skin after my quest to enable strictNullChecks ((https://github.com/signumsoftware/framework/commit/88ad60c81fa004ad095b75e101806f6159ef8999), (https://github.com/signumsoftware/framework/commit/b4804201e69be8cca2e005e537c4c3727b5d0803))

Just after turning the flag on, I’ve found 800 errors in a ~50 files project, of witch I’ve solved until now 600 (two evenings).

The investment necessary to introduce the flag is quite high. I was expecting something like 4 compile-time errors for each potential run-time bug fund… but it’s more like 50, and what is worst, the way to solve most of them is by adding | undefined and ! everywhere, making the code harder to read.

Here are some things that are creating me pain :

  • Deciding between null and undefined:. Since you took the safe/strict choice of not deciding what T? means, now every project has to do it.

Solution: I’m trying to follow Typescript and replace all our null by undefined but I’ve found some problems.

  • React render function accepts null but not undefined as a return value. It’s happy about undefined inside the React.Elements however. Quite odd…

Solution: By changing the .d.ts you get nice compile-time errors, but you’ll have to fix them manually.

Solution: I’m seriously thinking in throwing setState out the window and using forceUpdate everywhere to be able to define the state more accurately.

  • Receiving and sending Json objects: null and undefined are serialized differently,
JSON.stringify({}) //"{}"
JSON.stringify({ name: undefined }) //"{}"
JSON.stringify({ name: null }) //"{"name":null}"

For me this has important consequences, since in one case the property in the server side will be ignored (undefined) while in others will be set to null.

Solution: This forces me to treat null and undefined differently again and decorate the types accordingly. Fortunately they are auto-generated.

Solution: I hope async/await will solve this.

Sorry for my long rant, I’m a very enthusiastic Typescript and C# developer and I had lots of hopes in non-nullable references. I also develop the framework in my free time. If I consider this a tough pill to swallow, I can not imagine in a more enterprise environment…

What’s even worst, I think there is not too much that Typescript or any language that deals with the real world can do to solve the problem once and for all… looks to me that there are lots of variables that are nullable only in 5% of the cases, so the compiler gets a lot on the way to provide little help.

The expression

Object.keys(errors).map(k => errors[k])

has type any[]. Since undefined is a subtype of any, undefined is included.

In my quest to use strictNullChecks I’m finding some things that could be interesting. I’m going to write them all here but feel free to ask me to move to a different issue if the thing gets complicated:

  • React render() method throws an error if undefined is returned, but he is happy with null. This makes a nasty exception for my “replace all null by undefined” strategy… if you update render() in react.d.ts it becomes a compile time error, so it’s OK.
  • Speaking of .d.ts, is there any reference declaration type that is updated for strictNullChecks already?
  • In a world with strictNullChecks, is there any value on making non-nullable indexers, like this:
let error: { [member: string]: string }

there is always going to be some key for witch you won’t have a value…