The JavaScript runtime type system lacks generics, so .NET generic types and methods are projected
into JavaScript using a special convention. The projections are suffixed with the dollar ($
)
character, chosen because it is a valid identifier charater in JavaScript but not in C#, and
because it looks like an operator.
Note while TypeScript does have generics, they are merely a compile-time facade and do not have
any effect on runtime binding or execution. So it is unfortunately not possible in most cases
to directly map .NET generics to TypeScript generics. (The exception is when mapping .NET generic
collection interfaces to TypeScript generic collections like
Array<T>
and Map<TKey, TValue>
, which does work.)
// JavaScript
import dotnet from 'node-api-dotnet';
const System = dotnet.System;
System.Enum.Parse$(System.DayOfWeek)('Tuesday'); // Call generic method
System.Comparer$(System.DateTime).Create() // Call static method on generic class
const TaskCompletionSourceOfDate = System.TaskCompletionSource$(System.DateTime);
new TaskCompletionSourceOfDate(); // Create instance of generic class
new System.TaskCompletionSource(); // Create instance of non-generic class
A .NET generic method definition is projected as a function with a $
suffix. That function
takes generic type parameter(s) and returns the specialized generic function. So, .NET
Enum.Parse<T>()
is projected as Enum.Parse$(Type)
.
A .NET generic type definition is also projected as a function with a $
suffix. That function
takes generic type parameter(s) and returns the specialized generic type. So, .NET Comparer<T>
is projected as Comparer$(Type)
.
If a type has both generic and non-generic variants, the non-generic type is still available
normally, without any $
suffix. If a type has multiple generic variants then the one $
function
returns the requested type specialization according to the number of type arguments supplied.
Calling the toString()
method on the JS projection of any generic type definition, specialized
type, or non-generic type returns the full .NET type name. This may be helpful for diagnostics.
// JavaScript
System.Comparer$.toString(); // 'System.Comparer<T>'
System.Comparer$(System.DateTime).toString(); // 'System.Comparer<System.DateTime>
System.String.toString(); // 'System.String'
The above applies to dynamic binding, when the node-api-dotnet
library
can use reflection to locate generic type and method definitions, specialize them, and invoke them.
But some of that is impossible to do in an ahead-of-time compiled environment. Dynamically
specifying generic type arguments from JavaScript would require reflection and code-generation,
whch are not supported in an AOT executable. (In a pure C# application, the AOT compiler would be
able to know exactly what type arguments are used with any generic type or method, so it can
generate specialized code accordingly.)
This means that C# Node API modules compiled as AOT cannot export generic types or methods.
They can still use generic types in properties or methods, when the type argument is specified
ahead of time. For example it is OK to use AOT to export a method that has a parameter of type
KeyValuePair<string, int>
. But exporting a generic method with type parameter T and method
parameter KeyValuePair<string, T>
will not work with AOT.