{"id":35575972,"url":"https://github.com/perfectlynormal/typecontractor","last_synced_at":"2026-03-07T21:01:01.085Z","repository":{"id":195493572,"uuid":"574123570","full_name":"PerfectlyNormal/TypeContractor","owner":"PerfectlyNormal","description":"A dotnet tool for converting C# classes/enums to TypeScript interfaces/enums","archived":false,"fork":false,"pushed_at":"2025-10-11T15:26:49.000Z","size":459,"stargazers_count":0,"open_issues_count":13,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-19T15:09:31.085Z","etag":null,"topics":["code-generator","csharp","typescript"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PerfectlyNormal.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-12-04T13:58:47.000Z","updated_at":"2025-10-11T15:26:53.000Z","dependencies_parsed_at":"2026-01-05T07:00:17.327Z","dependency_job_id":null,"html_url":"https://github.com/PerfectlyNormal/TypeContractor","commit_stats":null,"previous_names":["perfectlynormal/typecontractor"],"tags_count":39,"template":false,"template_full_name":null,"purl":"pkg:github/PerfectlyNormal/TypeContractor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerfectlyNormal%2FTypeContractor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerfectlyNormal%2FTypeContractor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerfectlyNormal%2FTypeContractor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerfectlyNormal%2FTypeContractor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PerfectlyNormal","download_url":"https://codeload.github.com/PerfectlyNormal/TypeContractor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PerfectlyNormal%2FTypeContractor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30231474,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T19:01:10.287Z","status":"ssl_error","status_checked_at":"2026-03-07T18:59:58.103Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-generator","csharp","typescript"],"created_at":"2026-01-04T19:10:27.506Z","updated_at":"2026-03-07T21:01:01.073Z","avatar_url":"https://github.com/PerfectlyNormal.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TypeContractor\r\n\r\nLooks at one or more assemblies containing contracts and translates to\r\nTypeScript interfaces and enums\r\n\r\n## Goals\r\n\r\n1. Take one or more assemblies and reflect to find relevant types and their\r\n   dependencies that should get a TypeScript definition published.\r\n\r\n2. Perform replacements on the names to strip away prefixes\r\n\r\n   `MyCompany.SystemName.Common` is unnecessary to have in the output path. \r\n   We \r\n   can strip the common prefix and get `api/Modules/MyModule/SomeRequest.ts`\r\n   instead of \r\n   `api/MyCompany/SystemName/Common/Modules/MyModule/SomeRequest.ts`.\r\n\r\n3. Map custom types to their TypeScript-friendly counterparts if necessary.\r\n\r\n   For example, say your system has a custom `Money` type that maps down to\r\n   `number`. If we don't configure that manually, it will create the `Money`\r\n   interface, which only contains `amount` as a `number`. That's both\r\n   cumbersome to work with, as well as *wrong*, since the serialization will\r\n   (most likely) serialize `Money` as a `number`.\r\n\r\n4. Help validate responses from the API\r\n\r\n   Regular `response.json()` returns `any`, which means that any return type\r\n   we specify ourselves are just wishful thinking. It might match, it might\r\n   differ a bit, or it can differ a lot. Using [Zod](https://zod.dev/), we\r\n   generate schemas automatically that can be used for validating the\r\n   response and _knowing_ that we get the correct data back.\r\n\r\n5. Automatically generate API clients based on the controller endpoints.\r\n\r\n   Why bother creating a TypeScript API client where you have to manually\r\n   keep routes, parameters and everything in sync? We have the data, we have\r\n   the types, we have the schemas. Let's just make everything work together,\r\n   and let TypeContractor handle keeping those pesky API changes in sync.\r\n\r\n\r\n## Setup and configuration\r\n\r\nTo accomplish the same configuration and results as described under Goals,\r\ncreate Contractor like this:\r\n\r\n```csharp\r\nContractor.FromDefaultConfiguration(configuration =\u003e configuration\r\n            .AddAssembly(\"MyCompany.SystemName.Common\", \"MyCompany.SystemName.Common.dll\")\r\n            .AddCustomMap(\"MyCompany.SystemName.Common.Types.Money\", DestinationTypes.Number)\r\n            .StripString(\"MyCompany.SystemName.Common\")\r\n            .SetOutputDirectory(Path.Combine(Directory.GetCurrentDirectory(), \"api\")));\r\n```\r\n\r\n## Configuration file\r\n\r\nIt's possible to create a configuration file and provide all the same options\r\nvia a file instead of command line arguments.\r\n\r\nCreate a file called `typecontractor.config` in the same or a parent directory\r\nfrom where you run TypeContractor from. We use [dotnet-config] for parsing the\r\nfiles (written in [TOML]), so it's possible to inherit from parent directories,\r\na user-wide configuration or a system-wide configuration. Or have a\r\nrepository-specific configuration that can be overridden inside the repo with\r\n`typecontractor.config.user`. You can edit the file manually or install the \r\ndotnet-config tool for viewing and changing configuration similar to how\r\n`git config` works.\r\n\r\n### Example configuration file\r\n\r\n```toml\r\n[typecontractor]\r\n    # backslashes must be escaped\r\n    assembly = \"bin\\\\Debug\\\\net8.0\\\\MyCompany.SystemName.dll\"\r\n    output = \"api\"\r\n    strip = \"MyCompany\"\r\n    replace = \"MyCompany.Common:CommonFiles\" # Options can repeat\r\n    replace = \"ThirdParty.Http:Http\"\r\n    root = \"~/api\"\r\n    generate-api-clients = true\r\n    build-zod-schemas = true\r\n```\r\n\r\n## Run manually\r\n\r\nGet an instance of `Contractor` and call `contractor.Build();`\r\n\r\n## Integrate with ASP.NET Core\r\n\r\nThe easiest way is to TypeContractor, using `dotnet tool install --global\r\ntypecontractor`.  This adds `typecontractor` as an executable installed on the\r\nsystem and always available.\r\n\r\nRun `typecontractor` to get a list of available options.\r\n\r\nThis tool reflects over the main assembly provided and finds all controllers\r\n(that inherits from `Microsoft.AspNetCore.Mvc.ControllerBase`). Each controller\r\nis reflected over in turn, and finds all public methods that returns\r\n`ActionResult\u003cT\u003e`. The `ActionResult\u003cT\u003e` is unwrapped and the inner type `T` is\r\nadded to a list of candidates.\r\n\r\nAdditionally, the public methods returning an `ActionResult\u003cT\u003e` *or* a plain\r\n`ActionResult` will have their parameters analyzed as well. Anything that's not\r\na builtin type will be added to the list of candidates.\r\n\r\nMeaning if you have a method looking like:\r\n\r\n```csharp\r\npublic async Task\u003cActionResult\u003e Create([FromBody] CreateObjectDto request, CancellationToken cancellationToken)\r\n{\r\n   ...\r\n}\r\n```\r\n\r\nwe will add `CreateObjectDto` to the list of candidates.\r\n`System.Threading.CancellationToken` is a builtin type (currently, this means\r\nit is defined inside `System.`) and will be ignored. Same with other basic\r\ntypes such as `int`, `Guid`, `IEnumerable\u003cT\u003e` and so on.\r\n\r\nFor each candidate, we apply stripping and replacements and custom mappings and\r\nwrite everything to the output files.\r\n\r\n### Installing locally\r\n\r\nInstead of installing the tool globally, you can also add it locally to the\r\nproject that is going to use it. This makes it easier to make sure everyone who\r\nwants to run the project have it available.\r\n\r\nFor the initial setup, run:\r\n\r\n`dotnet new tool-manifest`\r\n`dotnet tool install typecontractor`\r\n\r\nin your Web-project.\r\n\r\nWhenever new users check out the repository, they can run `dotnet tool restore`\r\nand get everything you need installed.\r\n\r\n### Running automatically\r\n\r\nIn your `Web.csproj` add a target that calls the tool after build. Example:\r\n\r\n```xml\r\n\u003cTarget Name=\"GenerateTypes\" AfterTargets=\"Build\"\u003e\r\n  \u003cMessage Importance=\"high\" Text=\"Running in CI, not generating new types\" Condition=\"'$(AGENT_ID)' != ''\" /\u003e\r\n  \u003cMessage Importance=\"high\" Text=\"Generating API types\" Condition=\"'$(AGENT_ID)' == ''\" /\u003e\r\n  \u003cExec \r\n      Condition=\"'$(AGENT_ID)' == ''\"\r\n      ContinueOnError=\"true\"\r\n      Command=\"typecontractor --assembly $(OutputPath)$(AssemblyName).dll --output $(MSBuildThisFileDirectory)\\App\\src\\api --clean smart --replace My.Web.App.src.modules:App --replace Infrastructure.Common:Common --strip MyCompany\" /\u003e\r\n  \u003cMessage Importance=\"high\" Text=\"Finished generating API types\" Condition=\"'$(AGENT_ID)' == ''\" /\u003e\r\n\u003c/Target\u003e\r\n```\r\n\r\nThis will only run in non-CI environments (tested on Azure DevOps). Adjust the\r\nenvironment variable as needed. You don't want new types to be generated on the\r\nbuild machine, that should use whatever existed when the developer did their\r\nthing.\r\n\r\nIt will first:\r\n\r\n1. Strip out `MyCompany.` from the beginning of namespaces\r\n2. Replace `My.Web.App.src.modules` with `App`\r\n3. Replace `Infrastructure.Common` with `Common`\r\n\r\nby looking at the configured assembly. The resulting files are placed in\r\n`Web\\App\\src\\api`.\r\n\r\nWhen running with `--clean smart`, which is the default, it first generates the\r\nupdated or newly created files. After that, it looks in the output directory\r\nand removes every file and directory that are no longer needed.\r\n\r\nOther options are:\r\n\r\n* `none` -- which as the name suggests, does no cleanup at all.\r\n  That part is left as an exercise to the user.\r\n* `remove` -- which removes the entire output directory before\r\n  starting the file generation. This is probably the quickest, but some tools\r\n  that are watching for changes does not always react so well to having files\r\n  suddenly disappear and reappear.\r\n\r\n## Integration with Zod\r\n\r\nAn experimental option to generate [Zod schemas](https://zod.dev/) exists\r\nbehind the `--build-zod-schemas` flag. This causes each generated TypeScript\r\nfile to also have a `\u003cTypeName\u003eSchema` generated that can be integrated with\r\nZod. Currently no support for validations, but that might come in a future\r\nupdate.\r\n\r\nExample:\r\n\r\n```csharp\r\npublic class PaymentsPerYearResponse\r\n{\r\n    public IEnumerable\u003cint\u003e Years { get; set; } = [2023, 2024];\r\n    public Dictionary\u003cint, int\u003e PaymentsPerYear { get; set; } = new Dictionary\u003cint, int\u003e\r\n    {\r\n        { 2024, 4 },\r\n        { 2023, 12 }\r\n    };\r\n}\r\n```\r\n\r\ngenerates\r\n\r\n```typescript\r\nimport { z } from 'zod';\r\n\r\nexport interface PaymentsPerYearResponse {\r\n  years: number[];\r\n  paymentsPerYear: { [key: number]: number };\r\n}\r\n\r\nexport const PaymentsPerYearResponseSchema = z.object({\r\n  years: z.array(z.number()),\r\n  paymentsPerYear: z.record(z.string(), z.number()), // JavaScript is very stringy (https://zod.dev/?id=records)\r\n});\r\n```\r\n\r\nand can be integrated using something similar to\r\n\r\n```typescript\r\nconst response = await this.http.fetch('api/paymentsPerYear', { signal: cancellationToken });\r\nconst input = await response.json();\r\nreturn PaymentsPerYearResponseSchema.parse(input);\r\n```\r\n\r\nwhich will throw a `ZodError` if `input` fails to parse against the schema.\r\nOtherwise it returns a cleaned up version of `input`.\r\n\r\n## Automatic API client generation\r\n\r\nBy running TypeContractor with the `--generate-api-clients` flag, every\r\ncontroller found will have an automatic client generated for each of the\r\ndiscovered endpoints.\r\n\r\nShould be combined with the `--root` flag for generating nice imports.\r\nFor example `--root \"~/api\"` if you are writing files to `api` and have\r\n`~` defined as an alias in your bundler.\r\n\r\nIf Zod integration is enabled, the return type for each API call will\r\nbe automatically validated against the schema. If so, it expects\r\nthe `Response` prototype to be extended with a `parseJson` method.\r\nAn example implementation can be found in `tools/response.ts`.\r\n\r\nTo provide a custom template instead of using the built-in Aurelia one,\r\nprovide `--api-client-template` with the path to a Handlebars template\r\nthat does what you want. The available data model can be found in\r\n`TypeContractor/Templates/ApiClientTemplateDto.cs` and an example\r\ntemplate is `TypeContractor/Templates/aurelia.hbs`.\r\n\r\nSince the name is just the controller name with `Controller` replaced\r\nwith `Client`, collisions between controllers with the same name but\r\ndifferent namespaces are possible. If this happens, the first\r\ncontroller found gets to keep the original name and TypeContractor\r\nwill prefix the colliding client with the last part of its namespace.\r\nSo for example:\r\n\r\n`ExampleApp.Controllers.DataController` turns into `DataClient`,\r\nwhile `ExampleApp.Controllers.Subsystem.DataController` collides and\r\ngets turned into `SubsystemDataClient`.\r\n\r\n## Further customization using Annotations\r\n\r\nTo further customize the output of TypeContractor, you can install\r\nthe optional package `TypeContractor.Annotations` and start annotating\r\nyour controllers.\r\n\r\nAvailable annotations:\r\n\r\n* `TypeContractorIgnore`:\r\n  If you have a controller that doesn't need a client\r\n  generated, you can annotate that controller using `TypeContractorIgnore`\r\n  and it will be automatically skipped.\r\n* `TypeContractorName`:\r\n  If you have a badly named controller that you can't rename,\r\n  you want something custom, or just don't like the default naming\r\n  scheme, you can apply this attribute to select a brand new name.\r\n\r\n  If you have multiple endpoints with the same name and different parameters,\r\n  C# handles the overloads perfectly, but not so much in TypeScript.\r\n  Use `TypeContractorName` to rename a single endpoint to whatever fits.\r\n* `TypeContractorNullable`:\r\n  If your project doesn't support nullable reference types, or you just\r\n  feel like you know better, you can mark a property as nullable and\r\n  override the automatically detected setting.\r\n\r\n## Future improvements\r\n\r\n* Better documentation\r\n* Better performance -- if this should run on build, it can't take forever\r\n* Possible to add types to exclude?\r\n* Improve method for finding AspNetCore framework DLLs\r\n  * Possible to provide a manual path, so not a priority\r\n* Work with Hot Reload?\r\n\r\n[dotnet-config]: https://dotnetconfig.github.io/dotnet-config/index.html\r\n[TOML]: https://toml.io/en/\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fperfectlynormal%2Ftypecontractor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fperfectlynormal%2Ftypecontractor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fperfectlynormal%2Ftypecontractor/lists"}