Skip to content

Typeid support #2034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2025
Merged

Typeid support #2034

merged 2 commits into from
Jul 21, 2025

Conversation

Le0Developer
Copy link
Contributor

@Le0Developer Le0Developer commented May 12, 2025

See #2033

This is a very rough draft. I originally tried to use the x-typeid-type property I proposed in jetify-com/typeid#45. Then I noticed there's an IR involved and decided to temporarily extract the type name from the example field instead (which I also had to add to the parsers/IR).

I've tested this by modifying the openapi-ts-fetch example. I downloaded the openapi.yml file and modified the id property of Order to be a typeid like this:

        id:
          type: string
          format: typeid
          example: order_1111

And added typeids: true, to the openapi-ts.config.ts file.

This produced this output:

export type TypeID<T extends string> = `${T}_${string}`;

export type OrderId = TypeId<'order'>;

export type Order = {
  complete?: boolean;
  id?: OrderId;
  /* ... */
}

Nice 🎉

Currently the TypeID declaration is always added. Which means if you enable the config option but don't use typeids, you'll get an unused type warning.

Questions

  • I want to use x-typeid-type instead of parsing the example value, but I'm unsure given the current IR infrastructure. I don't believe adding arbitrary extensions to the parser is maintainable. One I had was: what if the IR had an extra field which simply contained the original object or just x-* properties? This feels more future-proof to me.
  • Should TypeID<T> be exported or should consumers do User["id"] instead of TypeID<"user"> if they want to access the type themselves?
  • Should there be a concrete type per ID (eg type UserID = "user_${string}") instead of the TypeID "overlord" (or maybe both)? This would require extending the "State" interface to keep track of already generated ID types

Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Copy link

changeset-bot bot commented May 12, 2025

🦋 Changeset detected

Latest commit: 257dd07

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

vercel bot commented May 12, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
hey-api-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 21, 2025 1:05pm

Copy link

pkg-pr-new bot commented May 12, 2025

Open in StackBlitz

npm i https://pkg.pr.new/hey-api/openapi-ts/@hey-api/nuxt@2034
npm i https://pkg.pr.new/hey-api/openapi-ts/@hey-api/openapi-ts@2034
npm i https://pkg.pr.new/hey-api/openapi-ts/@hey-api/vite-plugin@2034

commit: 257dd07

Copy link

codecov bot commented May 12, 2025

Codecov Report

Attention: Patch coverage is 0.81301% with 122 lines in your changes missing coverage. Please review.

Project coverage is 23.81%. Comparing base (8e1e6c9) to head (257dd07).
Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
...enapi-ts/src/plugins/@hey-api/typescript/plugin.ts 1.02% 97 Missing ⚠️
...s/openapi-ts/src/plugins/@hey-api/sdk/operation.ts 0.00% 6 Missing ⚠️
...pi-ts/src/plugins/@hey-api/typescript/operation.ts 0.00% 6 Missing ⚠️
...ugins/@tanstack/query-core/infiniteQueryOptions.ts 0.00% 4 Missing ⚠️
...ages/openapi-ts/src/openApi/2.0.x/parser/schema.ts 0.00% 3 Missing ⚠️
...ages/openapi-ts/src/openApi/3.0.x/parser/schema.ts 0.00% 3 Missing ⚠️
...ages/openapi-ts/src/openApi/3.1.x/parser/schema.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2034      +/-   ##
==========================================
- Coverage   23.89%   23.81%   -0.09%     
==========================================
  Files         317      317              
  Lines       29330    29450     +120     
  Branches     1229     1230       +1     
==========================================
+ Hits         7009     7014       +5     
- Misses      22312    22427     +115     
  Partials        9        9              
Flag Coverage Δ
unittests 23.81% <0.81%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Le0Developer
Copy link
Contributor Author

Hey @mrlubos before adding tests and solving the merge conflicts I was wondering what your thoughts were on the previously raised questions in the initial PR message?
It's currently getting the typeid prefix from the example field since adding x-typeid-prefix to the schema felt more awkward (since example could be reused and added to the doc comments in the future?).

Or alternatively should openapi-ts get a better plugin interface so this can be done externally?

@mrlubos
Copy link
Member

mrlubos commented Jul 1, 2025

Hi @Le0Developer, are you able to keep using the preview builds generated by this pull request? I will have some thoughts around developer experience but I think most of your ideas/questions are valid and need addressing, including a better plugin interface. Unfortunately, the current priorities lie elsewhere so I won't have time to address this improvement just yet, but wanted to give you at least some response

@mrlubos
Copy link
Member

mrlubos commented Jul 20, 2025

@Le0Developer are you still interested in this feature?

@Le0Developer
Copy link
Contributor Author

Yes! We are currently running the current state in prod and haven't had the opportunity to upgrade and deal with the other breaking changes (fetch client moving etc)

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

@Le0Developer do we even need a separate configuration option for the typeid format? We should simply understand it and handle it, what do you think?

@Le0Developer
Copy link
Contributor Author

Le0Developer commented Jul 21, 2025

@Le0Developer do we even need a separate configuration option for the typeid format? We should simply understand it and handle it, what do you think?

I've currently decided to do it like this because of the TypeID type that needs to be generated. Preferably it would be generated on use, but I don't know where to put that state. Alternatively we could of course just skip the generic and just do <id>_string directly.

This is semi-related to my 2nd question:

  1. Should TypeID be exported or should consumers do User["id"] instead of TypeID<"user"> if they want to access the type themselves?

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

For me it comes back to the question: would users ever end up with format: typeid without deliberately configuring it as you did? My hunch is the answer is a no, so we don't need to have a separate configuration option.

As for your state question, have a look at how it's handled in TanStack Query

const state: PluginState = {
hasCreateInfiniteParamsFunction: false,
hasCreateQueryKeyParamsFunction: false,
hasInfiniteQueries: false,
hasMutations: false,
hasQueries: false,
hasUsedQueryFn: false,
typeInfiniteData: undefined!,
};

You could create a similar state with a single boolean for TypeID and pass it around the plugin. We currently pass onRef that way, that could theoretically go into that state as well. It's not pretty, but it works for now.

RE exporting, I don't see any harm in exporting TypeID, but you'd know better than me whether it's useful. If it's not useful, don't export

@Le0Developer
Copy link
Contributor Author

Removed the option and added state tracking. I was hoping there'd be a nicer way than passing it around everywhere (eg on the plugin object or something), but alas.

Also exported the TypeID type.


Now that we have state tracking, number 3 is possible:

Should there be a concrete type per ID (eg type UserID = "user_${string}") instead of the TypeID "overlord" (or maybe both)? This would require extending the "State" interface to keep track of already generated ID types

We currently have typeid "redefinitions" (eg a bunch of export type UserID = TypeID<"user">) in a central file that we import. But I'd love if this can be automated as well.

Q: since this needs snake_case to CamelCase conversion, should I reuse the plugin.config.case option for this?

@Le0Developer
Copy link
Contributor Author

Implemented. This is how it's looking now:

diff --git a/examples/openapi-ts-fetch/src/client/types.gen.ts b/examples/openapi-ts-fetch/src/client/types.gen.ts
index 6d8a6e7b..e3ecbeef 100644
--- a/examples/openapi-ts-fetch/src/client/types.gen.ts
+++ b/examples/openapi-ts-fetch/src/client/types.gen.ts
@@ -2,7 +2,7 @@
 
 export type Order = {
   complete?: boolean;
-  id?: number;
+  id?: OrderId;
   petId?: number;
   quantity?: number;
   shipDate?: string;
@@ -694,6 +694,10 @@ export type UpdateUserResponses = {
   200: unknown;
 };
 
+export type TypeID<T extends string> = `${T}_${string}`;
+
+export type OrderId = TypeID<'order'>;
+
 export type ClientOptions = {
   baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {});
 };

From my perspective this is in a good state now (except that we parse the name from the example field but I don't know a better way).
Should I add tests now or do you have other comments?

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

Removed the option and added state tracking. I was hoping there'd be a nicer way than passing it around everywhere (eg on the plugin object or something), but alas.

Me too. We can explore it later if you want, this is usually faster and works.

But I'd love if this can be automated as well.

I'm not sure I understand what you're referring to!

Q: since this needs snake_case to CamelCase conversion, should I reuse the plugin.config.case option for this?

Think you already did this, looks good! Tests are the only remaining thing, do you need any guidance?

@Le0Developer
Copy link
Contributor Author

do you need any guidance?

Yes please, I don't see any obvious place for tests of the typescript "plugin" and can't find other tests.

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

There are three type-format.yaml test specs, one for every OpenAPI version. You can either add more schemas to those specs, or create a new spec format-typeid.yaml. You'll notice these tests are executed from plugins.test.ts, you can again add a new test case if necessary. The main thing I'd be looking for apart from the types is if it compiles with validators and transformers. Now that I look at it, we might even want to add TypeID support to validators, but we can leave that for a separate pull request if you want. Just think it would be nice to have a fuller support for this format

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

For your failing CI, run pnpm openapi-ts typecheck, that should tell you what's wrong. There's also a full pnpm typecheck command to catch other issues

@Le0Developer
Copy link
Contributor Author

Yeah there's a few places where state isn't being passed as expected, I'm working on it.

I'd love proper typeid support (with x-typepid-preifx or something) but it's not standardized, I agree that using example is a nasty hack (also just noticed it's not spec compliant because they can contain multiple _ whoops).

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

yeah I think we're a long way out from that. Happy to merge this once you got the tests and CI passing. Would love to know how much setup it's on the backend side to get the OpenAPI integration working?

@Le0Developer
Copy link
Contributor Author

Le0Developer commented Jul 21, 2025

Would love to know how much setup it's on the backend side to get the OpenAPI integration working?

Do you mean in general? We use typeid-go and SQLc for our database wrapper and native types in the database (CREATE TYPE user_id AS TEXT).

We tell SQLc to overwrite the types like this:

        overrides:
          - db_type: user_id
            go_type:
              type: UserID

And then have a struct like this:

type UserPrefix struct{}

func (UserPrefix) Prefix() string { return "user" }

type UserID struct {
	typeid.TypeID[UserPrefix]
}

SQLc will then use the custom type for all database queries. For user input we call typeid.Parse[db.UserID](...) and use typeid.New[db.UserID]() for creating new IDs.

So our models contain our custom UserID struct which is then picked up by swag when generating our OpenAPI docs.

@mrlubos
Copy link
Member

mrlubos commented Jul 21, 2025

@Le0Developer do you want to connect on Discord or somewhere else? I could probably use your help with Go

@Le0Developer
Copy link
Contributor Author

Le0Developer commented Jul 21, 2025

@Le0Developer do you want to connect on Discord or somewhere else? I could probably use your help with Go

Feel free to add me @leodeveloper (on Discord)

@mrlubos mrlubos merged commit f3d2d32 into hey-api:main Jul 21, 2025
15 of 17 checks passed
@Le0Developer Le0Developer deleted the feat/typeids branch July 21, 2025 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants