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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With