Consider:
- JavaScript has both
null
andundefined
primitive values, while .NET has onlynull
. - A default/uninitialized value in JS is
undefined
, while in .NET the default isnull
(for reference types andNullable<T>
value types). - In JavaScript,
typeof undefined === 'undefined'
andtypeof null === 'object'
.
So how should these inconsistencies in type systems of the two platforms be reconciled during automatic marshalling?
Note: Regardless of any approach taken here, .NET code has the option to fall back to working directly with
JSValue
and its distinctJSValue.Null
andJSValue.Undefined
values.
For discussion here, T
may any specific (not object/any
) type, including both
marshal-by-value (number, struct, enum) and marshal-by-ref (string, class, interface)
types.
If T
is not nullable (neither a Nullable<T>
value type nor a nullable reference
type), then the TypeScript projection will allow neither null
nor undefined
.
However, marshalling must still handle null .NET reference values even when the
type is non-nullable.
In any case a JS undefined
value passed to .NET is always converted to null
by
the marshaller.
If .NET null
values are marshalled as JS undefined
, that has the following effects:
Description | C# API | JS API | JS Notes |
---|---|---|---|
Method with optional param | void Method(T? p = null) |
method(p?: T): void |
p is undefined in JS if a .NET caller omitted the parameter or supplied null ;p is never null in JS when called by .NET. |
Method with nullable param | void Method(T? p) |
method(p: T | undefined): void |
p is undefined in JS if a .NET caller supplied null . |
Method with nullable return | T? Method() |
method(): T | undefined |
Result is undefined in JS if .NET method returned null ;result is never null when returned by .NET. |
Nullable property | T? Property |
property?: T |
Property value is undefined (but the property exists) in JS if the object was passed from .NET;value is never null (or missing) on an object from .NET. |
Alternatively, if .NET null
values are marshalled as JS null
, that has the following effects:
Description | C# API | JS API | JS Notes |
---|---|---|---|
Method with optional param | void Method(T? p = null) |
method(p: T | null): void |
p is null in JS if a .NET caller omitted the parameter or supplied null ;p is never undefined in JS when called by .NET. |
Method with nullable param | void Method(T? p) |
method(p?: T | null): void |
p is null in JS if a .NET caller supplied null . |
Method with nullable return | T? Method() |
method(): T | null |
Result is null in JS if .NET method returned null ;result is never undefined when returned by .NET. |
Nullable property | T? Property |
property: T | null |
Property value is null (and the property exists) in JS if the object was passed from .NET;value is never undefined (or missing) on an object from .NET. |
While null
and undefined
are often used interchangeably, the distinction can sometimes
be important. Let's analyze some ways in which null
and/or undefined
values might be
handled differently, and how common those practices are in JavaScript.
In JavaScript, there are several common ways to detect when an optional parameter was not supplied to a function:
function exampleFunction(optionalParameter?: any): void
-
Common / best practice: Check if the value type is equal to
'undefined'
.
if (typeof optionalParameter === 'undefined')
-
Somewhat common: Check if the value is strictly equal to
undefined
.
if (optionalParameter === undefined)
-
Common / best practice in TS & ES2020: Use the "nullish coalescing operator", which handles both
null
andundefined
:
value = optionalParameter ?? defaultValue
-
Traditional and still common (occasionally error-prone): Check if the value is falsy.
if (!optionalParameter)
value = optionalParameter || defaultValue
-
Less common: Check the length of the
arguments
object. (Use of the specialarguments
object is discouraged in modern JS, in favor of rest parameters.)
if (arguments.length === 0)
-
Uncommon: Check if the value is null with loose equality. It handles both
null
andundefined
becausenull == undefined
. (The loose equality operator is usually flagged by linters.)
if (optionalParameter == null)
A: null->undefined | B: null->null | |
---|---|---|
1 | ✅ | ❌ |
2 | ✅ | ❌ |
3 | ✅ | ✅ |
4 | ✅ | ✅ |
5 | ❌ | ❌ |
6 | ✅ | ✅ |
A JavaScript function my return undefined
, or null
, when it yields no result.
There is no strong consensus among the JS developer community
about when to use either one; some developers may prefer one or the other while
others may not think very hard about the distinction. There are a few ways the
caller might check the return value:
function exampleFunction(): any
- Traditional and still common (occasionally error-prone): Check if the result value is
falsy.
if (!result)
- Common: Check if the result value type is
'undefined'
or value is strictly equal toundefined
.
if (typeof result === 'undefined')
if (result === undefined)
- Uncommon: Check if the result value is null with loose equality.
if (result == null)
A: null->undefined | B: null->null | |
---|---|---|
1 | ✅ | ✅ |
2 | ✅ | ❌ |
3 | ✅ | ✅ |
In JavaScript, there are a few ways to detect when an optional property was not supplied with an object:
interface Example {
optionalProperty?: any;
}
- Common: Use the
in
operator.if ('optionalProperty' in exampleObject)
- Common: Use
hasOwnProperty
or the more modernhasOwn
replacement.
if (!exampleObject.hasOwnProperty('optionalProperty'))
if (!Object.hasOwn(exampleObject, 'optionalProperty'))
- Traditional and still common (occasionally error-prone): Check if the property value
is falsy.
if (!exampleObject.optionalProperty)
if (!exampleObject['optionalProperty'])
value = exampleObject.optionalProperty || defaultValue
- Less common: Check if the property value type is
'undefined'
or value is strictly equal toundefined
.
if (typeof exampleObject.optionalProperty === 'undefined')
if (exampleObject.optionalProperty === undefined)
- Less common: Use the nullish coalescing operator
value = exampleObject.optionalProperty ?? defaultValue
- Uncommon: Check if the property value is null with loose equality.
if (exampleObject.optionalProperty == null)
A: null->undefined | B: null->null | |
---|---|---|
1 | ❌ | ❌ |
2 | ❌ | ❌ |
3 | ✅ | ✅ |
4 | ✅ | ❌ |
5 | ✅ | ✅ |
6 | ✅ | ✅ |
Note even when marshalling null
to undefined
, common checks that rely on the
existince of properties can fail. And operations that enumerate the object properties
may have differing behavior for missing properties vs ones with undefined
value. For
more on that subtle distinction, see TypeScript's --exactOptionalPropertyTypes
option.
JavaScript code can specifically check if a parameter, return value, or property value is strictly equal to null:
if (value === null)
A: null->undefined | B: null->null |
---|---|
❌ | ✅ |
More experienced JavaScript developers never write such code, since
they are aware of the pervasiveness of undefined
. But it can be easy
for developers coming from other languages (like C# or Java) to write
such code while assuming null
works the same way, or merely from
muscle memory.
A JavaScript API could assign wholly different meanings to the two values, for
instance using undefined
to represent an uninitialized state and null
to
represent an intialized-but-cleared state. Since automatic marshalling of .NET
null
cannot support that distinction, calling such a JS API from .NET would
require direct use of JSValue.Undefined
and JSValue.Null
(or perhaps a
JS wrapper for the targeted API) to handle the disambiguation. But such an API
design aspect would likely confuse many JavaScript developers as well, so it
is not a common occurrence.
In the tables above, there are fewer ❌ marks in column A; this indicates
that mapping .NET null
to JS undefined
is the better choice for default
marshalling behavior.
There are a few rare cases in which the default may be problematic:
- Omitted optional function parameters, when the JS function body checks
arguments.length
. - Omitted optional properties of an object, when the JS code checks whether the object has the property, or enumerates the object properties.
- A nullable (not optional) value where the JS code checks for strict null equality.
To handle these cases (and any other situations that might arise), we can add
flags to the (planned)
[JSMarshalAs]
attribute to enable setting the null-value marshalling behavior
of a specific .NET method parameter, return value, or property to one of three
options:
undefined
(default)null
- omit - Exclude from the function arguments (if there are no non-omitted arguments after it), or exclude from the properties of the marshalled object.