2

It is fairly common to have methods where one or more parameter can be null or undefined (or often both with derived types). This can make for really long method signatures in some cases:

doThing(
  user: User | undefined | null, 
  thing: Thing | undefined | null, 
  ...moreArgs
): Result {
  // ...
}

I'm aware of a few ways to try to solve this problem, but both have downsides.

Optional parameters are great if the argument is truly optional, but feel awkward when the passed object can be null.

This is shorter but just looks wrong to me:

doThing(user?: User | null, thing?: Thing | null, ...moreArgs) 

The other way I know to fix this is with a generic:

type Nullable<T> = T | null | undefined

doThing(user: Nullable<User>, thing: Nullable<Thing>)

This is reasonable but I find that projects often end up with more than one generic of this type defined, they get imported from third party libraries, derived types end up having a trail of Maybe<T>, Nullable<T>, etc. Trying to keep this use standard across a large project can be really hard.

This seems like such a common use case that I would expect a more consistent solution. Is there a better way to do this?

Matt Sanders
  • 8,023
  • 3
  • 37
  • 49
  • 1
    What do you mean by "a more consistent solution"? Are you suggesting that a `Nullable` utility type should be provided by TypeScript itself? Such suggestions are regularly declined (see [ms/TS#39522](https://github.com/microsoft/TypeScript/issues/39522)) because they've learned that adding utility types is generally more harmful than helpful except in situations where such types are *necessary* for the compiler to emit declaration files. – jcalz Dec 13 '22 at 19:54
  • Does that fully address your question? If so I'll write up an answer explaining more fully; if not, what am I missing? – jcalz Dec 13 '22 at 19:55
  • 1
    `null` and `undefined` are different values with [different meanings](https://stackoverflow.com/a/57249968/4088472). Why not decide which one your API accepts and stick with it (I suggest `null` for intentionally blank, `undefined` for omitted)? – Slava Knyazev Dec 13 '22 at 20:10
  • 1
    Note that using optional parameters vs non-optional parameters of a type whose union includes `undefined` are **NOT** the same when using the compiler option [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes): https://tsplay.dev/NB48nW – jsejcksn Dec 13 '22 at 21:06
  • @jcalz - yep, I think that answers my question. I was hoping there was either a language construct that made creating my own generic unnecessary or a community convention that is so strong that I can comfortably use it everywhere. – Matt Sanders Dec 14 '22 at 18:21
  • @SlavaKnyazev - Yep, I'm versed in the difference between them. However just because I'm consistent with the APIs I create doesn't mean that the other APIs I consume are. – Matt Sanders Dec 14 '22 at 18:22
  • @jsejcksn - Great point. I usually run into this in cases where all arguments are supplied and I'm trying to reduce unnecessary verbosity. Using optional parameters as a way to do this feels like an anti-pattern to me but I mentioned it since it is one of the options I'm aware of. – Matt Sanders Dec 14 '22 at 18:24
  • I will write up an answer when I get a chance; it might be tomorrow because I'll be offline for a while – jcalz Dec 14 '22 at 22:56

1 Answers1

1

Unfortunately there is no built-in syntax that acts as a shorthand to writing the union of a type with null and undefined. If you want such behavior, you will need to either do it manually, or define or import a reusable utility type like

type Nullable<T> = T | null | undefined

For better or worse, TypeScript will not introduce such a utility type globally; this gets requested occasionally and consistently declined. See microsoft/TypeScript#39522 for example. Their reasoning is:

What we've heard from users is that they don't like it when we needlessly add new global types because a) it causes conflicts with theirs and b) we don't always pick the same definition that they were using.

Generally speaking they will only introduce utility types if such a type is necessary for automatically producing declaration files. I think the Omit<T, K> utility type was introduced to represent the behavior of the spread operator on generic types, but there were quite a few unhappy real world TS developers who were using their own slightly-different version of Omit which now. You can read more at this twitter thread which explains the pitfalls in more detail.

Since, so far at least, declaration files don't need a utility type to represent | null | undefined (it's hard to envision such a scenario where you couldn't just write | null | undefined in the declaration file), it's not worth it to the TS team to introduce this.


That leaves creating it yourself, or importing it from some well-known TS type library like ts-toolbelt's U.Nullable, or working with the other developers on a large project to adhere to some utility-type-related coding standards. None of these are great, but they're the best you can get in the absence of support from the language itself.

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks, this is super helpful and very thorough. Appreciate it! – Matt Sanders Dec 16 '22 at 04:49
  • They should have reserve some keywords to prevent such cases as JS do. – Experimenter Aug 11 '23 at 17:45
  • Reserving keywords would require that the TS team know which utility types they'd like to provide before they knew how to implement them (otherwise they'd just implement them directly and skip the time period during which they were reserved but not used). I don't see that as very plausible; should they have reserved, say, `Uppercase` in TS1.0 because TS4.1 would use it? How would they have known? – jcalz Aug 13 '23 at 20:31