Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In TypeScript, is there a difference between optional parameters and parameters than can be undefined?

Tags:

typescript

I was wondering if there was a difference between the these two bits of code:

function sayHello(name?: string) {
  if (name) { return 'Hello ' + name; }
  return 'Hello!';
}

and

function sayHello(name: string | undefined) {
  if (name) { return 'Hello ' + name; }
  return 'Hello!';
}

(I know that I couldn't put a second argument that isn't optional after the 'name' as it has to be the last or one of the last ones)

I was thinking about this earlier today, and I feel that the major difference for me first is what you're saying the consumer of the function.

The first is more implying optionality, like "you don't need to pass me this, but you can if you want" The second says "pass me a string, I don't care if it's undefined, I can handle that".

A similar thing can come up in interfaces and types, too.

interface Foo {
   thing?: string;
}

vs

interface Foo {
   thing: string | undefined;
}

Am I on the right track here? Anything else I should know?

like image 490
Sam Jarman Avatar asked Feb 01 '21 01:02

Sam Jarman


1 Answers

You're on the right track and pretty much correct.


In what follows, I am going to assume you are using the --strict or at least the --strictNullChecks compiler option so that undefined and null are not always implicitly allowed:

let oops: string = undefined; // error! 
// Type 'undefined' is not assignable to type 'string'

In TypeScript, a function/method parameter or object type's field that is marked as optional with the ? modifier means that it can be missing:

function opt(x?: string) { }

interface Opt {
    x?: string;
}

const optObj: Opt = {}; // okay
opt(); // okay

but such optional parameters/fields are also allowed to be present but undefined:

const optObj2: Opt = { x: undefined } // okay
opt(undefined); // okay

In fact, if you use IntelliSense to examine the types of such optional parameters/fields, you'll see that the compiler automatically adds undefined as a possibility:

function opt(x?: string) { }
// function opt(x?: string | undefined): void

interface Opt {
    x?: string;
}
type AlsoOpt = Pick<Opt, "x">;
/* type AlsoOpt = {
    x?: string | undefined;
} */

From the point of view of the implementer of the function or the consumer of the object type, the optional element can be treated as if it is always present, but possibly undefined:

function opt(x?: string) {
    // (parameter) x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

function takeOpt(v: Opt) {
    const x = v.x;
    // const x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

Compare and contrast this with a required (non-optional) field or parameter that includes | undefined:

function req(x: string | undefined) { }

interface Req {
    x: string | undefined
}

Like optional versions, the required one with | undefined accepts an explicit undefined. But unlike the optional versions, the required ones cannot be called or created with the value missing entirely:

req(); // error, Expected 1 arguments, but got 0!
req(undefined); // okay
const reqObj: Req = {}; // error, property x is missing!
const reqObj2: Req = { x: undefined } // okay

And, like with the optional versions, the implementer of the function or the consumer of the object type will see the optional things as definitely present but possibly undefined:

function req(x: string | undefined) {
    // (parameter) x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

function takeReq(v: Req) {
    const x = v.x;
    // const x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

Other things to note:


There are also optional elements in tuple types which work the same way. They are like optional object fields but they have the same restriction as parameters: if any tuple element is optional, all subsequent ones also have to be optional:

type OptTuple = [string, number?];
const oT: OptTuple = ["a"]; // okay
const oT2: OptTuple = ["a", undefined]; // okay

type ReqTuple = [string, number | undefined];
const rT: ReqTuple = ["a"]; // error! Source has 1 element(s) but target requires 2
const rT2: ReqTuple = ["a", undefined]; // okay

For function parameters, you can also sometimes use the type void to mean "missing", and therefore | void to mean "optional", as implemented in microsoft/TypeScript#27522. So x?: string and x: string | void are treated similarly:

function orVoid(x: string | void) {
    console.log((typeof x !== "undefined" ? x.toUpperCase() : "undefined"));
}
orVoid(); // okay

This is not yet the case for object fields. It has been implemented in microsoft/TypeScript#40823, but has not yet made it into the language (and I'm not sure if it will ever do so):

interface OrVoid {
    x: string | void;
}
const o: OrVoid = {} // error! x is missing

Finally, I'll point you to microsoft/TypeScript#13195, an issue discussing the funny historical relationship in TypeScript between "missing" and "present but undefined". Sometimes they are treated identically, other times they are distinguished.

One major place where people would like more distinction is that when something is optional, developers are not always happy about the possibility of explicitly passing in undefined. That is, people would like to say that interface Opt {x?: string} should mean that x is either a string, or it is missing entirely. They think that if you have a value o of type Obj, then o.x === undefined should only happen when "x" in o is false.

But that's not what happens by default in TypeScript. In most compilr configurations, o.x === undefined does not let you know whether or not the property is present-but-undefined or missing. And so "x" in o doesn't conclusively tell you whether o.x is a string or undefined.

There is a new --exactOptionalPropertyTypes compiler flag slated to be released with TypeScript 4.4 which, when enabled, will not add undefined to the domain of optional properties, and so o.x === undefined implies that the x property is missing. This compiler option will not be enabled by default, though, and it only applies to properties and not function parameters.

In any case, I'd suggest avoiding situations where the difference between missing and undefined matters.


Playground link to code

like image 178
jcalz Avatar answered Nov 15 '22 07:11

jcalz