From 1126ff737e081aa2e15772459c9f70d23747d8b3 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 00:23:12 +0100 Subject: [PATCH 01/15] nuget workflow fix permissions and early version check --- .github/workflows/publish.yml | 46 +++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 65480e2..794ec76 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,26 +17,17 @@ on: tags: - "v[0-9]+.[0-9]+.[0-9]+*" -jobs: - build: - name: ๐Ÿ› ๏ธ Build and Pack - uses: ./.github/workflows/main.yml +permissions: + checks: write + pull-requests: write - publish: - name: ๐Ÿ“ฆ Publish to NuGet - needs: build +jobs: + version: + name: Determine Version runs-on: ubuntu-latest - permissions: - contents: read + outputs: + version: ${{ steps.ver.outputs.version }} steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: src/global.json - - name: Determine and validate version id: ver shell: pwsh @@ -63,6 +54,25 @@ jobs: Write-Host "Version: $ver" "version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + build: + name: ๐Ÿ› ๏ธ Build and Pack + uses: ./.github/workflows/main.yml + + publish: + name: ๐Ÿ“ฆ Publish to NuGet + needs: [version, build] + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: src/global.json + - name: Download pack artifact uses: actions/download-artifact@v4 with: @@ -71,7 +81,7 @@ jobs: - name: Pack TypeShim env: - PKG_VERSION: ${{ steps.ver.outputs.version }} + PKG_VERSION: ${{ needs.version.outputs.version }} run: > dotnet pack src/TypeShim/TypeShim.csproj -c Release From 86a0ea2d751051ad2e97db2cf740dbcc1b2fed11 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 00:29:40 +0100 Subject: [PATCH 02/15] restore --- .github/workflows/publish.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 794ec76..71cbc6c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,7 +55,7 @@ jobs: "version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append build: - name: ๐Ÿ› ๏ธ Build and Pack + name: Main uses: ./.github/workflows/main.yml publish: @@ -72,6 +72,9 @@ jobs: uses: actions/setup-dotnet@v4 with: global-json-file: src/global.json + + - name: Restore + run: dotnet restore src/TypeShim/TypeShim.csproj - name: Download pack artifact uses: actions/download-artifact@v4 From 1ca3641c18884328ca13d148be36d6c35b8c2b1f Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 19:50:04 +0100 Subject: [PATCH 03/15] inline version --- .github/workflows/publish.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 71cbc6c..24a4a2f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -83,14 +83,12 @@ jobs: path: src/TypeShim/bin/pack - name: Pack TypeShim - env: - PKG_VERSION: ${{ needs.version.outputs.version }} run: > dotnet pack src/TypeShim/TypeShim.csproj -c Release -o .\.artifacts --no-build - -p:Version=$env:PKG_VERSION + -p:Version=${{ needs.version.outputs.version }} -p:ContinuousIntegrationBuild=true - name: Push to GitHub Packages From 84ad20892575c90dbc5ab3d8f0216f335f083912 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 20:44:13 +0100 Subject: [PATCH 04/15] no symbols in release --- src/TypeShim/TypeShim.csproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/TypeShim/TypeShim.csproj b/src/TypeShim/TypeShim.csproj index 204e0b9..3dbff7b 100644 --- a/src/TypeShim/TypeShim.csproj +++ b/src/TypeShim/TypeShim.csproj @@ -7,7 +7,9 @@ enable enable en - + none + false + TypeShim @@ -15,7 +17,7 @@ ArcadeMode TypeShim Typesafe .NET โ†”๏ธŽ TypeScript interop - wasm, csharp, typescript, interop, generated + wasm, csharp, typescript, interop, generator https://github.com/ArcadeMode/TypeShim https://github.com/ArcadeMode/TypeShim MIT From 8793c6cfe3026610c17e338b70d35e573aced3a9 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 20:45:18 +0100 Subject: [PATCH 05/15] no typeshim lib between generators --- src/TypeShim/build.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/TypeShim/build.ps1 b/src/TypeShim/build.ps1 index 652b4d0..e5c4c35 100755 --- a/src/TypeShim/build.ps1 +++ b/src/TypeShim/build.ps1 @@ -15,9 +15,6 @@ if (Test-Path $OutputDir) { New-Item -ItemType Directory -Force -Path "$OutputDir\analyzers" | Out-Null New-Item -ItemType Directory -Force -Path "$OutputDir\build" | Out-Null -Write-Host "" -Write-Host "Building TypeShim" -ForegroundColor Yellow -dotnet publish $TypeShimProject -c Release -o "$OutputDir\build" Write-Host "" Write-Host "Building Generator (JIT)" -ForegroundColor Yellow dotnet publish $GeneratorProject -c Release -o "$OutputDir\build" /p:NativeMode=false From 2978ae6655e88583b1d1d5eb61fe5bda2e20da7a Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 20:45:55 +0100 Subject: [PATCH 06/15] download generators and analyzers, build lib for pack only --- .github/workflows/publish.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 24a4a2f..f6136b0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -76,11 +76,18 @@ jobs: - name: Restore run: dotnet restore src/TypeShim/TypeShim.csproj - - name: Download pack artifact + - name: Download all RID artifacts uses: actions/download-artifact@v4 with: - pattern: typeshim-pack - path: src/TypeShim/bin/pack + pattern: pack-build-* + path: pack + merge-multiple: true + + - name: Build TypeShim library + run: > + dotnet build src/TypeShim/TypeShim.csproj + -c Release + --no-restore - name: Pack TypeShim run: > From 5ca75648293386dd7467f48b808c3ca7922e65da Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 20:46:32 +0100 Subject: [PATCH 07/15] remove obsolete artifact combine --- .github/workflows/main.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4893f25..057ccfc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,26 +98,6 @@ jobs: !**/*.dSYM/** if-no-files-found: error retention-days: 1 - - combine: - name: ๐Ÿ“ฆ Publish Artifacts - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Download all RID artifacts - uses: actions/download-artifact@v4 - with: - pattern: pack-build-* - path: pack - merge-multiple: true - - - name: Upload combined pack directory - uses: actions/upload-artifact@v4 - with: - name: typeshim-pack - path: pack/** publish-results: name: ๐Ÿงพ Publish Results From 739f190d450e8b6a4d72ccbcd65923acc0b898d6 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 20:47:07 +0100 Subject: [PATCH 08/15] extract badge and move to publish workflow --- .github/workflows/main.yml | 38 +++++++++++++++-------------------- .github/workflows/publish.yml | 21 ++++++++++++++++--- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 057ccfc..a9b166b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,12 +1,15 @@ name: Main on: - workflow_dispatch: workflow_call: + outputs: + badge-color: + description: 'Color for the test results badge' + value: ${{ jobs.publish-results.outputs.badge-color }} + badge-message: + description: 'Text for the test results badge' + value: ${{ jobs.publish-results.outputs.badge-message }} pull_request: - push: - branches: - - master permissions: checks: write @@ -104,6 +107,9 @@ jobs: runs-on: ubuntu-latest needs: build if: ${{ always() && !cancelled() }} + outputs: + badge-color: ${{ steps.badge.outputs.color }} + badge-message: ${{ steps.badge.outputs.message }} steps: - name: Download test results uses: actions/download-artifact@v4 @@ -120,29 +126,17 @@ jobs: results/**/e2e-report-*.xml - name: Determine badge content - if: ${{ github.event_name == 'push' }} + id: badge shell: bash run: | - conclusion='${{ fromJSON(steps.test-results.outputs.json).conclusion }}' - if [ "$conclusion" = "success" ]; then - echo "BADGE_COLOR=#26cc23" >> "$GITHUB_ENV" - echo "BADGE_MESSAGE=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_succ }} passing" >> "$GITHUB_ENV" + if [ "${{ fromJSON(steps.test-results.outputs.json).conclusion }}" = "success" ]; then + "color=#26cc23" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "message=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_succ }} passing" | Out-File -FilePath $env:GITHUB_OUTPUT -Append else - echo "BADGE_COLOR=#cc5623" >> "$GITHUB_ENV" - echo "BADGE_MESSAGE=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_fail }}/${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} failing" >> "$GITHUB_ENV" + "color=#cc5623" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "message=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_fail }}/${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} failing" | Out-File -FilePath $env:GITHUB_OUTPUT -Append fi - - name: Update badge - if: ${{ github.event_name == 'push' }} - uses: schneegans/dynamic-badges-action@v1.7.0 - with: - auth: ${{ secrets.GIST_TOKEN }} - gistID: '0f24ed28316a25f6293d5771a247f19d' - filename: typeshim-tests-badge.json - label: Tests - message: ${{ env.BADGE_MESSAGE }} - color: ${{ env.BADGE_COLOR }} - benchmarks: name: ๐Ÿ Benchmark Generator runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f6136b0..52d087d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish NuGet +name: Publish on: workflow_dispatch: @@ -109,5 +109,20 @@ jobs: if: ${{ inputs.dry-run }} uses: actions/upload-artifact@v4 with: - name: typeshim-nuget-package-${{ steps.ver.outputs.version }} - path: .\.artifacts\*.nupkg \ No newline at end of file + name: typeshim-nupkg-${{ needs.version.outputs.version }} + path: .\.artifacts\*.nupkg + + badge: + name: ๐Ÿท๏ธ Update Version Badge + needs: build + runs-on: ubuntu-latest + steps: + - name: Update badge + uses: schneegans/dynamic-badges-action@v1.7.0 + with: + auth: ${{ secrets.GIST_TOKEN }} + gistID: '0f24ed28316a25f6293d5771a247f19d' + filename: typeshim-tests-badge.json + label: Tests + message: ${{ needs.build.outputs.badge-message }} + color: ${{ needs.build.outputs.badge-color }} \ No newline at end of file From 9f163c5e13b5782f403f8d6f9bcb97c3e694074a Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 21:05:34 +0100 Subject: [PATCH 09/15] keep lib with generators --- .github/workflows/publish.yml | 4 ++-- src/TypeShim/build.ps1 | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 52d087d..9699474 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,12 +4,12 @@ on: workflow_dispatch: inputs: version: - description: 'Tag to publish v[0-9]+.[0-9]+.[0-9]+*' + description: 'Tag to publish v1.2.3[-prerelease]' required: true default: '' type: string dry-run: - description: 'Perform a dry run without pushing to NuGet' + description: 'Dry run' required: false default: false type: boolean diff --git a/src/TypeShim/build.ps1 b/src/TypeShim/build.ps1 index e5c4c35..652b4d0 100755 --- a/src/TypeShim/build.ps1 +++ b/src/TypeShim/build.ps1 @@ -15,6 +15,9 @@ if (Test-Path $OutputDir) { New-Item -ItemType Directory -Force -Path "$OutputDir\analyzers" | Out-Null New-Item -ItemType Directory -Force -Path "$OutputDir\build" | Out-Null +Write-Host "" +Write-Host "Building TypeShim" -ForegroundColor Yellow +dotnet publish $TypeShimProject -c Release -o "$OutputDir\build" Write-Host "" Write-Host "Building Generator (JIT)" -ForegroundColor Yellow dotnet publish $GeneratorProject -c Release -o "$OutputDir\build" /p:NativeMode=false From 2a540a036c61e2dedbbc4b23cd2f3eb7bc5019c0 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 21:14:59 +0100 Subject: [PATCH 10/15] pwsh --- .github/workflows/main.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a9b166b..de90cf7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -127,15 +127,12 @@ jobs: - name: Determine badge content id: badge - shell: bash + shell: pwsh run: | - if [ "${{ fromJSON(steps.test-results.outputs.json).conclusion }}" = "success" ]; then - "color=#26cc23" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - "message=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_succ }} passing" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - else - "color=#cc5623" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - "message=${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_fail }}/${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} failing" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - fi + $path = if ("${{ fromJSON(steps.test-results.outputs.json).conclusion }}" -eq "success") { '#26cc23' } else { '#cc5623' } + $message = if ("${{ fromJSON(steps.test-results.outputs.json).conclusion }}" -eq "success") { "${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_succ }} passing" } else { "${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs_fail }}/${{ fromJSON( steps.test-results.outputs.json ).formatted.stats.runs }} failing" } + "color=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "message=$message" | Out-File -FilePath $env:GITHUB_OUTPUT -Append benchmarks: name: ๐Ÿ Benchmark Generator From 2d251ae8990f4aa14b676ad45b0266458513f793 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 21:27:59 +0100 Subject: [PATCH 11/15] explicit package name --- .github/workflows/publish.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9699474..5466e06 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -93,7 +93,7 @@ jobs: run: > dotnet pack src/TypeShim/TypeShim.csproj -c Release - -o .\.artifacts + -o . --no-build -p:Version=${{ needs.version.outputs.version }} -p:ContinuousIntegrationBuild=true @@ -101,7 +101,8 @@ jobs: - name: Push to GitHub Packages if: ${{ !inputs.dry-run }} run: > - dotnet nuget push .\.artifacts\*.nupkg + dotnet nuget push + TypeShim.${{ needs.version.outputs.version }}.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json @@ -110,7 +111,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: typeshim-nupkg-${{ needs.version.outputs.version }} - path: .\.artifacts\*.nupkg + path: TypeShim.${{ needs.version.outputs.version }}.nupkg badge: name: ๐Ÿท๏ธ Update Version Badge From c663f0363039705b6cd9f19fa85bc0b9d032e395 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 22:35:06 +0100 Subject: [PATCH 12/15] Update docs and include readme in pack --- .github/workflows/publish.yml | 2 +- README.md | 49 ++++++++++++++++++++++++++----- src/TypeShim/TsExportAttribute.cs | 10 +++---- src/TypeShim/TypeShim.csproj | 2 ++ 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5466e06..a823fab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -114,7 +114,7 @@ jobs: path: TypeShim.${{ needs.version.outputs.version }}.nupkg badge: - name: ๐Ÿท๏ธ Update Version Badge + name: ๐Ÿท๏ธ Update Readme Badge needs: build runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 321d62d..b0b60b6 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@

TypeShim

- Strongly-typed .NET-JS interop facade generation + .NET WebAssembly meets TypeScript

Test status ## Why TypeShim -With TypeShim you can use your .NET WASM project with TypeScript _without having to write a single JSExport yourself_. All interop code (C# & TS) is generated at build-time to provide reliability and type safety. TypeShim's class level exports make for a natural programming experience with familiar syntax. No bells and whistles, just your .NET WASM project accessible through a neat TypeScript facade. +With TypeShim you can use your .NET WebAssembly project with TypeScript without having to write any interop code. All interop code (C# & TS) is generated at build-time to provide reliability and type safety. TypeShim's class level exports make for a natural programming experience with familiar syntax. It just makes your .NET classes available in TypeScript before you even had a chance to snap your fingers. -Simply install TypeShim in your .NET WASM project, drop a `[TSExport]` on your C# classes and _voilร _: .NET-JS interop is generated at build time including a powerful TypeScript library matching your C# classes exactly. +Simply install TypeShim in your .NET WebAssembly project, drop a `[TSExport]` on your C# classes and _voilร _: .NET-JS interop is generated at build time including a powerful TypeScript library matching your C# classes exactly. ## Features at a glance -- ๐Ÿญ No-nonsense [interop](#concepts) generation. -- ๐ŸŒฑ Opt-in with just one attribute. - ๐Ÿค– Export full classes: constructors, methods and properties. -- ๐Ÿ’ฐ [Enriched type marshalling](#enriched-type-support). -- ๐Ÿ›ก Type-safety across the interop boundary. +- ๐Ÿ’ฐ [Rich type support](#enriched-type-support). +- ๐Ÿญ No-nonsense [interop](#concepts) generation. +- ๐Ÿ›ก Type-safety by generating both sides of the interop boundary. - ๐Ÿ‘ [Easy setup](#installing) +- โšก _Fast_: AOT compiled, benchmarked and optimized. ## Samples Samples below demonstrate the same operations when interfacing with TypeShim generated code vs `JSExport` generated code. Either way you will load your wasm browser app as [described in the docs](https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop/wasm-browser-app?view=aspnetcore-10.0#javascript-interop-on-). The runtime created by `dotnet.create()` can be passed directly into the provided `TypeShimInitializer`'s `initialize` method. The initializer exists so that helper functions for type marshalling can be set up and a reference to the assembly exports can be retrieved for the generated types to use internally. @@ -294,6 +294,8 @@ TypeShim aims to continue to broaden its type support. Suggestions and contribut | `Dictionary` | `?` | ๐Ÿ’ก | under consideration | | `(T1, T2)` | `[T1, T2]` | ๐Ÿ’ก | under consideration | +Table 1. TypeShim supported interop types + | .NET Marshalled Type | Mapped Type | Support | Note | |----------------------|-------------|--------|------| | `Boolean` | `Boolean` | โœ… | | @@ -328,6 +330,8 @@ TypeShim aims to continue to broaden its type support. Suggestions and contribut | `Func` | `Function`| โœ… | | | `Func` | `Function` | โœ… | | +Table 2. TypeShim support for .NET-JS interop types + *For `[TSExport]` classes ## Run the sample @@ -357,6 +361,8 @@ TypeShim is configured through MSBuild properties, you may provide these through | `TypeShim_GeneratedDir` | `TypeShim` | Directory path (relative to `IntermediateOutputPath`) for generated `YourClass.Interop.g.cs` files. | `TypeShim` | | `TypeShim_MSBuildMessagePriority` | `Normal` | MSBuild message priority. Set to High for debugging. | `Low`, `Normal`, `High` | +Table 3. Configuration options + ### Limitations TSExports are subject to minimal, but some, constraints. @@ -365,6 +371,35 @@ TSExports are subject to minimal, but some, constraints. - By default, JSExport yields value semantics for Array instances, this is one reference type that is atypical. It is under consideration to be addressed but an effective alternative is to define your own List class to preserve reference semantics. - Classes with generic type parameters can not be part of interop codegen at this time. +### Performance + +TypeShim has been optimized to achieve average codegen times of ~1 ms per class in a set of benchmarks going up to 200 classes. By optimizing the implementation and providing NativeAOT builds via the NuGet package, most users should see end-to-end codegen times of roughly 50โ€“200 ms for projects with 25โ€“200 classes. Every PR validates both AOT and JIT performance to help maintain these numbers. + +Performance is prioritized to minimize build-time impact and deliver the best possible experience for TypeShim users. Secondly it was a good excuse to play around with profiling tools and get some hands on experience with performance optimization and NativeAOT. + +The earlier versions of TypeShim used regular JIT builds which suffered expensive runtime start times and an inability to warm-up so even smaller projects would require more than 1 second for codegen. Switching to NativeAOT brought this down to the quarterisecond range and after several optimizations has been reduced to below a tenth of a second in many cases. + +Results from the continuous benchmarking that is now part of every pull request are shown in Table 4. The 0 classes case demonstrates the overhead of starting the process without doing any work. + +| Method | Compilation | ClassCount | Mean | Error | StdDev | +|--------- |------------ |-----------:|------------:|----------:|----------:| +| **Generate** | **AOT** | **0** | **14.02 ms** | **1.319 ms** | **0.873 ms** | +| **Generate** | **AOT** | **1** | **31.35 ms** | **0.969 ms** | **0.641 ms** | +| **Generate** | **AOT** | **10** | **31.82 ms** | **1.683 ms** | **1.113 ms** | +| **Generate** | **AOT** | **25** | **45.32 ms** | **1.565 ms** | **1.035 ms** | +| **Generate** | **AOT** | **50** | **56.50 ms** | **1.103 ms** | **0.730 ms** | +| **Generate** | **AOT** | **100** | **91.60 ms** | **2.294 ms** | **1.517 ms** | +| **Generate** | **AOT** | **200** | **93.92 ms** | **1.553 ms** | **1.027 ms** | +| **Generate** | **JIT** | **0** | **42.07 ms** | **0.687 ms** | **0.454 ms** | +| **Generate** | **JIT** | **1** | **813.62 ms** | **10.321 ms** | **6.827 ms** | +| **Generate** | **JIT** | **10** | **814.93 ms** | **9.107 ms** | **6.024 ms** | +| **Generate** | **JIT** | **25** | **862.08 ms** | **11.549 ms** | **7.639 ms** | +| **Generate** | **JIT** | **50** | **900.00 ms** | **14.144 ms** | **9.355 ms** | +| **Generate** | **JIT** | **100** | **1,014.10 ms** | **12.046 ms** | **7.968 ms** | +| **Generate** | **JIT** | **200** | **986.96 ms** | **22.021 ms** | **14.565 ms** | + +Table 4. Benchmark results on an AMD EPYC 7763 2.45GHz Github Actions runner. + ## Contributing Contributions are welcome. diff --git a/src/TypeShim/TsExportAttribute.cs b/src/TypeShim/TsExportAttribute.cs index ea518be..040ce62 100644 --- a/src/TypeShim/TsExportAttribute.cs +++ b/src/TypeShim/TsExportAttribute.cs @@ -4,22 +4,22 @@ /// TSExport classes must be non-static. ///
///
-/// TSExport classes define your interop API surface. You can use static and instance members in your TSExport classes, you will use them as static or instance in TypeScript too. -/// Whenever a method returns an instance of a TSExport class, TypeShim generates a TypeScript proxy class that wraps the interop calls to the underlying C# instance. +/// TSExport classes define your .NET-TS API surface. Public methods, properties and constructors, both static and member, are accessible from the generated TypeScript library.
+/// Whenever a TSExport class crosses the interop boundary, TypeShim automatically wraps it in an appropriate TypeScript class matching the C# class's public signature. /// /// [TSExport] /// public class MyCounter /// { -/// public static MyCounter GetCounter() => new MyCounter(); /// public int Count { get; private set; } = 0; /// public string Increment() => $"Hello from C#, Count is now {++Count}"; +/// public bool EqualsCount(MyCounter other) => Count == other.Count; /// } /// /// -/// Usage in TypeScript looks like this: +/// Can be used in TypeScript like this: /// /// -/// const counter: MyCounter.Proxy = MyCounter.GetCounter(); +/// const counter: MyCounter = new MyCounter(); /// console.log(counter.Count); // 0 /// console.log(counter.Increment()); // "Hello from C#, Count is now 1" /// console.log(counter.Count); // 1 diff --git a/src/TypeShim/TypeShim.csproj b/src/TypeShim/TypeShim.csproj index 3dbff7b..7f4afdf 100644 --- a/src/TypeShim/TypeShim.csproj +++ b/src/TypeShim/TypeShim.csproj @@ -21,9 +21,11 @@ https://github.com/ArcadeMode/TypeShim https://github.com/ArcadeMode/TypeShim MIT + README.md
+ From 22d5e5caacc88721adfa0eea0b82283b379a848a Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Thu, 26 Feb 2026 22:44:40 +0100 Subject: [PATCH 13/15] fix pack dir --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a823fab..71ced48 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -80,7 +80,7 @@ jobs: uses: actions/download-artifact@v4 with: pattern: pack-build-* - path: pack + path: src/TypeShim/bin/pack merge-multiple: true - name: Build TypeShim library From 204021ca6e3df1de3de4e8f572b9e9e84aa68668 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Fri, 27 Feb 2026 00:13:26 +0100 Subject: [PATCH 14/15] comments and cleanup debug target messages --- src/TypeShim/TsExportAttribute.cs | 2 ++ src/TypeShim/TypeShim.targets | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/TypeShim/TsExportAttribute.cs b/src/TypeShim/TsExportAttribute.cs index 040ce62..01a05af 100644 --- a/src/TypeShim/TsExportAttribute.cs +++ b/src/TypeShim/TsExportAttribute.cs @@ -23,6 +23,8 @@ /// console.log(counter.Count); // 0 /// console.log(counter.Increment()); // "Hello from C#, Count is now 1" /// console.log(counter.Count); // 1 +/// const counter2: MyCounter = new MyCounter(); +/// console.log(counter.EqualsCount(counter2)); // false /// /// [AttributeUsage(AttributeTargets.Class)] diff --git a/src/TypeShim/TypeShim.targets b/src/TypeShim/TypeShim.targets index c9b5fa9..bb0ecbb 100644 --- a/src/TypeShim/TypeShim.targets +++ b/src/TypeShim/TypeShim.targets @@ -60,7 +60,7 @@ - + <_TypeShim_TypeScriptOutputFilePathSegment>$([System.IO.Path]::Combine('$(TypeShim_TypeScriptOutputDirectory)', '$(TypeShim_TypeScriptOutputFileName)')) @@ -68,8 +68,8 @@ <_TypeShim_CSharpOutputDirectoryPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)', '$(TypeShim_GeneratedDir)')) - - + + @@ -80,11 +80,11 @@ Command=""$(_TypeShimGeneratorExePath)" "@(Compile->'%(FullPath)', ';')" "$(_TypeShim_CSharpOutputDirectoryPath)" "$(_TypeShim_TypeScriptOutputFilePath)" "$(_TargetingPackRefDir)"" WorkingDirectory="$(ProjectDir)" IgnoreExitCode="false" /> - + - + @@ -98,6 +98,6 @@ - + \ No newline at end of file From 095cef1bfc4d1b00e4f42d8f22251d7127ee5888 Mon Sep 17 00:00:00 2001 From: Marc Zwart Date: Fri, 27 Feb 2026 00:47:28 +0100 Subject: [PATCH 15/15] readme --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b0b60b6..8db3795 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@

TypeShim

- .NET WebAssembly meets TypeScript + Bridge .NET WebAssembly and TypeScript with fast, reliable, and type-safe codegen.

Test status ## Why TypeShim -With TypeShim you can use your .NET WebAssembly project with TypeScript without having to write any interop code. All interop code (C# & TS) is generated at build-time to provide reliability and type safety. TypeShim's class level exports make for a natural programming experience with familiar syntax. It just makes your .NET classes available in TypeScript before you even had a chance to snap your fingers. +With TypeShim you can interop between .NET WebAssembly and TypeScript with class level exports. It takes just one `[TSExport]` to bring your class to TypeScript with all its constructors, methods and properties. TypeShim aims to provide a natural programming experience with familiar syntax, without any unnecessary bells and whistles. It just makes your .NET classes available in TypeScript, no hassle. -Simply install TypeShim in your .NET WebAssembly project, drop a `[TSExport]` on your C# classes and _voilร _: .NET-JS interop is generated at build time including a powerful TypeScript library matching your C# classes exactly. +The interop code on both the C# & TypeScript side is build-time generated in a flash to provide an up-to-date and type safe interop boundary that just _works_. Thoroughly tested from codegen to runtime to deliver reliability. -## Features at a glance +## At a glance -- ๐Ÿค– Export full classes: constructors, methods and properties. -- ๐Ÿ’ฐ [Rich type support](#enriched-type-support). -- ๐Ÿญ No-nonsense [interop](#concepts) generation. -- ๐Ÿ›ก Type-safety by generating both sides of the interop boundary. +- ๐Ÿ“ค Class level exports. +- ๐Ÿ’Ž [Rich type support](#enriched-type-support). +- โœ No-nonsense [interop](#concepts) codegen. +- ๐Ÿฆพ Thoroughly validated for correctness +- โšก Tuned for [high performance](#performance) - ๐Ÿ‘ [Easy setup](#installing) -- โšก _Fast_: AOT compiled, benchmarked and optimized. ## Samples Samples below demonstrate the same operations when interfacing with TypeShim generated code vs `JSExport` generated code. Either way you will load your wasm browser app as [described in the docs](https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop/wasm-browser-app?view=aspnetcore-10.0#javascript-interop-on-). The runtime created by `dotnet.create()` can be passed directly into the provided `TypeShimInitializer`'s `initialize` method. The initializer exists so that helper functions for type marshalling can be set up and a reference to the assembly exports can be retrieved for the generated types to use internally.