You're on the right path and quite accurate.
For the purposes ahead, let's assume you have enabled the --strict
or at least the --strictNullChecks
compiler option to disallow implicit permission for undefined
and null
:
let oops: string = undefined; // error!
// Type 'undefined' is not assignable to type 'string'
In TypeScript, when a function/method parameter or object type field is denoted as optional using the ?
modifier, it indicates that it can be absent:
function opt(x?: string) { }
interface Opt {
x?: string;
}
const optObj: Opt = {}; // okay
opt(); // okay
However, these optional parameters/fields can also be present but undefined
:
const optObj2: Opt = { x: undefined } // okay
opt(undefined); // okay
When examining the types of such optional parameters/fields using IntelliSense, you'll notice that the compiler automatically considers 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 both the function implementer's and the object type consumer's perspective, the optional element can be handled as if always present but potentially undefined
:
function opt(x?: string) {
console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}
function takeOpt(v: Opt) {
const x = v.x;
console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}
Differentiating this scenario from a required (non-optional) field or parameter that incorporates | undefined
:
function req(x: string | undefined) { }
interface Req {
x: string | undefined
}
Similar to their optional counterparts, required fields with | undefined
accept an explicit undefined
. However, they cannot operate without passing the value entirely missing:
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
Like before, the implementer of the function or the consumer of the object type will view the required elements as definitely present but possibly undefined
:
function req(x: string | undefined) {
console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}
function takeReq(v: Req) {
const x = v.x;
console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}
Additional points to consider:
Tuple types also include optional elements, working in a similar manner to optional object fields. Nonetheless, they follow the restriction where if any tuple element is optional, all subsequent ones must also 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
With function parameters, the void
type can sometimes signify "missing," hence utilizing | void
to denote "optional." This was introduced in microsoft/TypeScript#27522. Thus, x?: string
and x: string | void
are handled similarly:
function orVoid(x: string | void) {
console.log((typeof x !== "undefined") ? x.toUpperCase() : "undefined");
}
orVoid(); // okay
While applicable for function parameters, this feature has not been extended to object fields yet. It was proposed in microsoft/TypeScript#40823 but remains pending implementation into the language:
interface OrVoid {
x: string | void;
}
const o: OrVoid = {} // error! x is missing
Lastly, I recommend avoiding scenarios where differentiating between missing and undefined
holds significance.
If interested, explore more about the upcoming changes concerning optional property types in TypeScript through this --exactOptionalPropertyTypes compiler flag soon to be included in TypeScript 4.4, providing distinctions regarding optional properties' presence or absence.
Access the code in Playground