Using .NET Types from TypeScript
April 19, 2026 · View on GitHub
This guide covers using existing .NET types directly from TypeScript code. This enables you to leverage the full .NET Base Class Library (BCL) and third-party .NET libraries from your TypeScript programs.
Overview
SharpTS supports two forms of .NET interop:
| Direction | Description | Use Case |
|---|---|---|
| Outbound | Compile TypeScript to .NET DLLs | Consume TS libraries from C# |
| Inbound | Use .NET types from TypeScript | Access BCL and .NET libraries |
This guide covers inbound interop - calling .NET code from TypeScript using the @DotNetType decorator.
Prerequisites
- .NET 10.0 SDK
- Decorators enabled (default, or pass
--experimentalDecoratorsfor Legacy mode)
@DotNetType works in both execution modes:
| Mode | Notes |
|---|---|
Compiled (--compile) | Overload resolution runs at compile time using TypeScript static types. IL-level Callvirt/Newobj directly invoke the resolved method. |
| Interpreted (default) | Overload resolution runs per call against the actual runtime argument types. Resolved MethodInfos are cached on the wrapper so repeated calls avoid reflection lookup. |
Use @DotNetOverload(...) when runtime resolution can't pick the overload you want — see Overload Hints.
Note: Decorators are enabled by default (TC39 Stage 3). Use
--experimentalDecoratorsfor Legacy (Stage 2) decorators, or--noDecoratorsto disable decorator support.
Quick Start
// Declare an external .NET type
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
append(value: string): StringBuilder;
toString(): string;
}
// Use it like a native TypeScript class
let sb = new StringBuilder();
sb.append("Hello, ");
sb.append("World!");
console.log(sb.toString()); // Output: Hello, World!
Compile and run:
sharpts --compile example.ts
dotnet example.dll
Basic Usage
The @DotNetType Decorator
The @DotNetType decorator binds a TypeScript class declaration to an existing .NET type:
@DotNetType("Fully.Qualified.TypeName")
declare class TypeScriptName {
// Method and property signatures
}
- First argument: The fully-qualified .NET type name (e.g.,
System.Text.StringBuilder) declare class: Indicates this is an external type with no implementation in TypeScript
Declaring External Types
Use declare class to define the TypeScript interface for a .NET type. You only need to declare the members you intend to use:
@DotNetType("System.Guid")
declare class Guid {
static newGuid(): Guid;
static parse(input: string): Guid;
toString(): string;
}
// You don't need to declare every method - just what you use
let id = Guid.newGuid();
console.log(id.toString());
Supported Member Types
| Member Type | TypeScript Syntax | Example |
|---|---|---|
| Constructor | constructor(params) | constructor(capacity: number) |
| Instance method | methodName(params): ReturnType | append(value: string): StringBuilder |
| Static method | static methodName(params): ReturnType | static newGuid(): Guid |
| Instance property | propertyName: Type | length: number |
| Readonly property | readonly propertyName: Type | readonly length: number |
| Static property | static propertyName: Type | static readonly now: DateTime |
Type Mapping
TypeScript to .NET Type Conversion
When calling .NET methods, SharpTS automatically converts TypeScript types:
| TypeScript Type | .NET Type | Notes |
|---|---|---|
number | double | Default numeric mapping |
number | int, long, float, byte | Narrowing conversion when method expects it |
string | string | Direct mapping |
boolean | bool | Direct mapping |
object | object | Dynamic fallback |
Naming Conventions
TypeScript uses camelCase while .NET uses PascalCase. SharpTS handles this automatically:
| .NET Method | TypeScript Declaration |
|---|---|
Append() | append() |
GetValue() | getValue() |
ToString() | toString() |
NewGuid() | newGuid() |
When you declare methods, use camelCase names. SharpTS resolves them to the PascalCase .NET equivalents.
Overload Resolution
.NET methods often have multiple overloads. SharpTS uses cost-based resolution to select the best match:
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
// Declare the overloads you need
append(value: string): StringBuilder;
append(value: number): StringBuilder;
append(value: boolean): StringBuilder;
toString(): string;
}
let sb = new StringBuilder();
sb.append("text"); // Calls Append(string)
sb.append(42); // Calls Append(double)
sb.append(true); // Calls Append(bool)
Resolution priority (lower cost = preferred):
- Exact type match (e.g.,
number→double) - Lossless conversion (e.g.,
number→float) - Narrowing conversion (e.g.,
number→int) - Object fallback (any →
object)
The same cost scale is used in both compiled and interpreted modes, so an overload that resolves in one mode resolves the same way in the other — as long as the argument types are unambiguous.
Overload Hints
When a method has multiple overloads that a TypeScript call can't distinguish
(e.g., you want ToInt32(int) instead of the default ToInt32(double)), use
@DotNetOverload("<signature>") to pin the target signature:
@DotNetType("System.Convert")
declare class Convert {
// Without the hint, runtime picks ToInt32(double) for a TS number.
// The hint narrows to ToInt32(int) — truncates 3.7 to 3 instead of
// rounding to 4.
@DotNetOverload("int")
static toInt32(value: number): number;
}
console.log(Convert.toInt32(3.7)); // 3
The hint value is a comma-separated list of parameter types matching the
overload's signature. Recognized aliases: int, long, short, byte, sbyte,
uint, ulong, ushort, float/single, double, decimal, bool/boolean,
char, string, object, plus their System.* equivalents. Other types can
be named by their fully-qualified CLR name.
Use @DotNetOverload("constructor-sig") on a declared constructor to pin the
constructor overload similarly.
Examples
StringBuilder (Instance Methods and Chaining)
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
append(value: string): StringBuilder;
append(value: number): StringBuilder;
append(value: boolean): StringBuilder;
readonly length: number;
toString(): string;
}
let sb = new StringBuilder();
sb.append("Name: ");
sb.append("Alice");
sb.append(", Age: ");
sb.append(30);
sb.append(", Active: ");
sb.append(true);
console.log(sb.toString()); // Name: Alice, Age: 30, Active: True
console.log(sb.length); // 34
Guid (Static Methods)
@DotNetType("System.Guid")
declare class Guid {
static newGuid(): Guid;
static parse(input: string): Guid;
static readonly empty: Guid;
toString(): string;
}
let id = Guid.newGuid();
console.log(id.toString()); // e.g., "a1b2c3d4-..."
let parsed = Guid.parse("00000000-0000-0000-0000-000000000000");
console.log(parsed.toString()); // 00000000-0000-0000-0000-000000000000
DateTime (Static Properties)
@DotNetType("System.DateTime")
declare class DateTime {
static readonly now: DateTime;
static readonly utcNow: DateTime;
static readonly today: DateTime;
readonly year: number;
readonly month: number;
readonly day: number;
readonly hour: number;
readonly minute: number;
toString(): string;
}
let now = DateTime.now;
console.log(now.year); // e.g., 2024
console.log(now.month); // e.g., 12
console.log(now.day); // e.g., 25
TimeSpan (Value Types)
@DotNetType("System.TimeSpan")
declare class TimeSpan {
static fromSeconds(value: number): TimeSpan;
static fromMinutes(value: number): TimeSpan;
static fromHours(value: number): TimeSpan;
static fromDays(value: number): TimeSpan;
add(ts: TimeSpan): TimeSpan;
readonly totalSeconds: number;
readonly totalMinutes: number;
readonly totalHours: number;
toString(): string;
}
let duration = TimeSpan.fromMinutes(90);
console.log(duration.totalHours); // 1.5
console.log(duration.totalSeconds); // 5400
let extra = TimeSpan.fromMinutes(30);
let total = duration.add(extra);
console.log(total.totalMinutes); // 120
Convert (Type Conversion)
@DotNetType("System.Convert")
declare class Convert {
static toInt32(value: number): number;
static toInt32(value: string): number;
static toDouble(value: string): number;
static toBoolean(value: number): boolean;
static toString(value: boolean): string;
}
let rounded = Convert.toInt32(42.7); // 43
let parsed = Convert.toDouble("3.14159"); // 3.14159
let flag = Convert.toBoolean(1); // true
let text = Convert.toString(true); // "True"
String.Format (Params Arrays)
@DotNetType("System.String")
declare class String {
static format(format: string, ...args: object[]): string;
static concat(str0: string, str1: string): string;
static isNullOrEmpty(value: string): boolean;
}
let message = String.format("Hello {0}, you have {1} messages!", "Alice", 5);
console.log(message); // Hello Alice, you have 5 messages!
let formatted = String.format("{0} + {1} = {2}", 10, 20, 30);
console.log(formatted); // 10 + 20 = 30
Mixing External and Local Types
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
append(value: string): StringBuilder;
toString(): string;
}
// Regular TypeScript class
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
toFormattedString(): string {
// Use .NET StringBuilder inside TypeScript class
let sb = new StringBuilder();
sb.append("Person { name: ");
sb.append(this.name);
sb.append(", age: ");
sb.append(this.age.toString());
sb.append(" }");
return sb.toString();
}
}
let person = new Person("Bob", 25);
console.log(person.toFormattedString()); // Person { name: Bob, age: 25 }
Advanced Features
Method Chaining
Methods that return this or the same type support chaining:
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
append(value: string): StringBuilder;
appendLine(): StringBuilder;
appendLine(value: string): StringBuilder;
toString(): string;
}
let result = new StringBuilder()
.append("Line 1")
.appendLine()
.append("Line 2")
.toString();
Multiple External Types
You can declare and use multiple .NET types in the same file:
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
append(value: string): StringBuilder;
toString(): string;
}
@DotNetType("System.Guid")
declare class Guid {
static newGuid(): Guid;
toString(): string;
}
// Use both together
let sb = new StringBuilder();
sb.append("ID: ");
sb.append(Guid.newGuid().toString());
console.log(sb.toString());
Properties vs Methods
.NET properties are accessed without parentheses, methods require them:
@DotNetType("System.Text.StringBuilder")
declare class StringBuilder {
constructor();
readonly length: number; // Property - access as sb.length
toString(): string; // Method - call as sb.toString()
}
let sb = new StringBuilder();
console.log(sb.length); // Property access (no parentheses)
console.log(sb.toString()); // Method call (parentheses required)
Generating Declarations
SharpTS can auto-generate TypeScript declarations from .NET types using the DeclarationGenerator:
From Individual Types
var generator = new DeclarationGenerator();
string declaration = generator.GenerateForType("System.Text.StringBuilder");
Console.WriteLine(declaration);
Output:
@DotNetType("System.Text.StringBuilder")
export declare class StringBuilder {
constructor();
constructor(capacity: number);
constructor(value: string);
append(value: string): StringBuilder;
append(value: number): StringBuilder;
append(value: boolean): StringBuilder;
appendLine(): StringBuilder;
appendLine(value: string): StringBuilder;
insert(index: number, value: string): StringBuilder;
remove(startIndex: number, length: number): StringBuilder;
replace(oldValue: string, newValue: string): StringBuilder;
clear(): StringBuilder;
toString(): string;
readonly length: number;
readonly capacity: number;
}
Type Mapping in Generated Declarations
The generator automatically maps .NET types to TypeScript:
| .NET Type | TypeScript Type |
|---|---|
void | void |
string | string |
bool | boolean |
int, long, double, float, decimal | number |
object | unknown |
DateTime | Date |
Task | Promise<void> |
Task<T> | Promise<T> |
List<T>, T[] | T[] |
Dictionary<K,V> | Map<K, V> |
HashSet<T> | Set<T> |
Nullable<T> | T | null |
Limitations
The following .NET features are not currently supported:
| Feature | Status | Notes |
|---|---|---|
| Generic types | Not supported | Cannot declare List<T> directly |
ref / out parameters | Not supported | Methods with ref/out params cannot be called |
| Events | Supported (both modes) | Use addEventListener/removeEventListener — see Events |
| Delegates | Supported (both modes) | TS functions auto-convert to delegate params — see Delegates |
| Indexers | Not supported | Cannot use obj[index] syntax |
| Operators | Not supported | Operator overloads not accessible |
| Extension methods | Not supported | Must call as static methods |
| Nullable value types | Partial | Generated as T | null but runtime behavior varies |
Workarounds
For unsupported features, consider:
- Creating a C# wrapper class that exposes a simpler API
- Using reflection-based interop via compiled TypeScript (see .NET Integration Guide)
Delegates and Callbacks
Any TypeScript function can be passed where a .NET method expects a delegate — works in both interpreter and compiled modes. The interpreter builds a shim on demand so the delegate's Invoke signature round-trips through the TS callable:
@DotNetType("System.Collections.Generic.List`1")
declare class IntList {
constructor();
add(item: number): void;
forEach(action: (item: number) => void): void; // Action<int>
findAll(predicate: (item: number) => boolean): IntList; // Predicate<int>
}
let items = new IntList();
items.add(1); items.add(2); items.add(3);
items.forEach((n) => console.log(n));
Supported delegate shapes:
| .NET type | TS shape |
|---|---|
Action | () => void |
Action<T1…> | (a: T1, …) => void |
Func<TResult> | () => TResult |
Func<T1…, TResult> | (a: T1, …) => TResult |
Predicate<T> | (a: T) => boolean |
EventHandler | (sender: any, args: any) => void |
EventHandler<T> | (sender: any, payload: T) => void |
Incoming .NET values are normalized for TypeScript on entry to the callback
(integral numerics → number, complex objects → wrapped instance). The return
value is converted back to the delegate's declared return type; a throw inside
the TS callback propagates synchronously to the .NET caller.
Threading contract
Main-thread only. Delegate shims run the TS callable on whatever thread invoked the delegate. The interpreter is not thread-safe, so invoking a shim off the SharpTS event-loop thread (e.g., from a
Timer, aTaskcontinuation, or a background thread) is undefined behavior — races, corrupted state, or crashes are possible.A future release may introduce an opt-in marshalling hint (e.g.,
@DotNetCallback("marshal")) that hops off-thread invocations back to the event loop. Today, keep delegate sinks synchronous and on-thread.
Events
Works in both interpreter and compiled modes.
TypeScript has no syntax for += on .NET events, so SharpTS exposes a DOM-style
API on any @DotNetType-wrapped instance or class:
@DotNetType("System.Timers.Timer")
declare class Timer {
constructor(interval: number);
start(): void;
stop(): void;
addEventListener(
name: string,
handler: (sender: any, args: any) => void
): void;
removeEventListener(
name: string,
handler: (sender: any, args: any) => void
): void;
}
- Event names use the PascalCase .NET name (e.g.,
"Elapsed","StringReceived"). addEventListenerlooks up theEventInfoby name and wires a delegate shim.removeEventListenermust receive the same function reference originally passed toaddEventListener— the subscription is keyed by that reference so the underlyingRemoveEventHandlercall can find the matching shim.- Static events work the same way:
ClassName.addEventListener("Name", handler). - Subscribing the same
(name, handler)pair twice is idempotent.
The threading contract above applies: if the .NET event fires from a background
thread, the handler will be invoked on that thread. Prefer event sources that
fire on the event-loop thread, or wrap .NET APIs in a helper that re-raises
events on the main thread.
Exception Mapping
When a .NET method called through @DotNetType throws, SharpTS translates the
exception to a JavaScript-style error so try/catch in TypeScript works naturally.
The original .NET exception is preserved on e.cause for diagnostics.
| .NET exception | JS error (e.name) |
|---|---|
ArgumentNullException | TypeError |
ArgumentException | TypeError |
InvalidCastException | TypeError |
NullReferenceException | TypeError |
ArgumentOutOfRangeException | RangeError |
IndexOutOfRangeException | RangeError |
OverflowException | RangeError |
DivideByZeroException | RangeError |
FormatException | SyntaxError |
| (everything else) | Error |
TargetInvocationException is unwrapped before classification, so the mapped
error reflects the actual failure, not the reflection wrapper.
@DotNetType("System.Guid")
declare class Guid {
static parse(input: string): Guid;
}
try {
Guid.parse("not-a-guid");
} catch (e) {
console.log(e.name); // "SyntaxError" (FormatException → SyntaxError)
console.log(e.message); // .NET's original message
// e.cause holds the original System.FormatException
}
Currently the mapping is applied in interpreter mode. Compiled mode propagates
the raw .NET exception — DotNetExceptionMapper.ClassifyAsJsErrorName is a
public entry point so compiled-mode callers can opt into the same classification.
Troubleshooting
Type Not Found
Error: .NET type 'X' not found
- Ensure the type name is fully qualified (e.g.,
System.Text.StringBuilder, notStringBuilder) - The type must be in an assembly loaded by the runtime (BCL types are always available)
Method Not Found
Error: Method resolution fails at runtime
- Check that your camelCase declaration matches the PascalCase .NET method
- Verify the parameter types match what the .NET method expects
- Some .NET methods may have different overloads than expected
Decorator Not Recognized
Error: Unknown decorator: DotNetType
- Decorators are enabled by default. If you used
--noDecorators, remove that flag. @DotNetTypeis a built-in compiler decorator, not a user-defined one
Type Not Found at Runtime
Error: @DotNetType: .NET type '…' not found in any loaded assembly.
In interpreted mode the type is resolved at the point the declare class
statement executes, from the set of assemblies currently loaded into the
process. If your type lives in a third-party assembly, make sure it's
loaded before the script runs — e.g., reference it from the host app or
Assembly.LoadFrom it up front.
See Also
- .NET Integration Guide - Compiling TypeScript for C# consumption
- Execution Modes - Interpreted vs compiled mode details
- Code Samples - TypeScript to C# mappings