tsoa
tsoa copied to clipboard
Feature: inline type aliases (@tsoaInline)
Sorting
-
I'm submitting a ...
- [ ] bug report
- [X] feature request
- [ ] support request
-
I confirm that I
- [X] used the search to make sure that a similar issue hasn't already been submit
I did, and I found a lot of other similar issues, for example :
- #798
- #751
Repro project
https://github.com/jeremyVignelles/repro-tsoa/tree/repro/pick-partial-omit
Expected Behavior
Expected swagger.json
{
"components": {
"examples": {},
"headers": {},
"parameters": {},
"requestBodies": {},
"responses": {},
"schemas": {
"User": {
"properties": {
"id": {
"type": "number",
"format": "double"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"Happy",
"Sad"
]
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"id",
"email",
"name",
"phoneNumbers"
],
"type": "object",
"additionalProperties": false
},
"UserCreationParams": {
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"email",
"name",
"phoneNumbers"
],
"type": "object"
},
"UserPatchParams": {
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"type": "string",
"enum": [
"Happy",
"Sad"
]
}
},
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "repro-tsoa",
"version": "1.0.0",
"license": {
"name": "MIT"
},
"contact": {
"name": "John doe",
"email": "[email protected]"
}
},
"openapi": "3.0.0",
"paths": {
"/users/{userId}": {
"get": {
"operationId": "GetUser",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
},
"security": [],
"parameters": [
{
"in": "path",
"name": "userId",
"required": true,
"schema": {
"format": "double",
"type": "number"
}
},
{
"in": "query",
"name": "name",
"required": false,
"schema": {
"type": "string"
}
}
]
},
"patch": {
"operationId": "PatchUser",
"responses": {
"204": {
"description": "No content"
}
},
"security": [],
"parameters": [
{
"in": "path",
"name": "userId",
"required": true,
"schema": {
"format": "double",
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserPatchParams"
}
}
}
}
}
},
"/users": {
"post": {
"operationId": "CreateUser",
"responses": {
"201": {
"description": "Created"
}
},
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreationParams"
}
}
}
}
}
}
},
"servers": [
{
"url": "/"
}
]
}
/**
* @tsoaInline
*/
export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;
/**
* @tsoaInline
*/
export type UserPatchParams = Partial<Omit<User, "id">>
Current Behavior
Generated swagger.json
{
"components": {
"examples": {},
"headers": {},
"parameters": {},
"requestBodies": {},
"responses": {},
"schemas": {
"User": {
"properties": {
"id": {
"type": "number",
"format": "double"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"Happy",
"Sad"
]
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"id",
"email",
"name",
"phoneNumbers"
],
"type": "object",
"additionalProperties": false
},
"Pick_User.email-or-name-or-phoneNumbers_": {
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"email",
"name",
"phoneNumbers"
],
"type": "object",
"description": "From T, pick a set of properties whose keys are in the union K"
},
"UserCreationParams": {
"$ref": "#/components/schemas/Pick_User.email-or-name-or-phoneNumbers_"
},
"Partial_Omit_User.id__": {
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"type": "string",
"enum": [
"Happy",
"Sad"
]
}
},
"type": "object",
"description": "Make all properties in T optional"
},
"UserPatchParams": {
"$ref": "#/components/schemas/Partial_Omit_User.id__"
}
},
"securitySchemes": {}
},
"info": {
"title": "repro-tsoa",
"version": "1.0.0",
"license": {
"name": "MIT"
},
"contact": {
"name": "John doe",
"email": "[email protected]"
}
},
"openapi": "3.0.0",
"paths": {
"/users/{userId}": {
"get": {
"operationId": "GetUser",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
},
"security": [],
"parameters": [
{
"in": "path",
"name": "userId",
"required": true,
"schema": {
"format": "double",
"type": "number"
}
},
{
"in": "query",
"name": "name",
"required": false,
"schema": {
"type": "string"
}
}
]
},
"patch": {
"operationId": "PatchUser",
"responses": {
"204": {
"description": "No content"
}
},
"security": [],
"parameters": [
{
"in": "path",
"name": "userId",
"required": true,
"schema": {
"format": "double",
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserPatchParams"
}
}
}
}
}
},
"/users": {
"post": {
"operationId": "CreateUser",
"responses": {
"201": {
"description": "Created"
}
},
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreationParams"
}
}
}
}
}
}
},
"servers": [
{
"url": "/"
}
]
}
Note that the type alias are generated as
"UserCreationParams": {
"$ref": "#/components/schemas/Pick_User.email-or-name-or-phoneNumbers_"
},
which could be simplified as client code generators (like NSwag) are unable to handle this properly.
Possible Solution
As mentionned above, there could be a JSDoc annotation that would allow to inline the type alias to flatten its type aliases indirections.
Steps to Reproduce
- clone https://github.com/jeremyVignelles/repro-tsoa/tree/repro/pick-partial-omit
- yarn build
- See build/swagger.json
Context (Environment)
Version of the library: 3.5.2 Version of NodeJS: 14
- Confirm you were using yarn not npm: [X]
Detailed Description
The jsdoc annotation can be placed on a type declaration, like so:
export interface User {
id: number;
email: string;
name: string;
status?: "Happy" | "Sad";
phoneNumbers: string[];
}
/**
* A post request should not contain an id.
* @tsoaInline
*/
export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;
If both types are used, the above code would generate the same thing as if they were in fact two separate interfaces, that is, the same as :
export interface User {
id: number;
email: string;
name: string;
status?: "Happy" | "Sad";
phoneNumbers: string[];
}
/**
* A post request should not contain an id.
*/
export interface UserCreationParams {
email: string;
name: string;
phoneNumbers: string[];
}
Breaking change?
No, the new syntax would be opt-in
There is a workaround for this, although not pretty. You first need to make sure that you only use interfaces on your API, so replace
export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;
by
export interface UserCreationParams extends Pick<User, "email" | "name" | "phoneNumbers"> {};
Then, you need to flatten the base type on the right, for which you can use a helper type like this:
type Expand<T> = { [K in keyof T]: T[K] };
export interface UserCreationParams extends Expand<Pick<User, "email" | "name" | "phoneNumbers">> {};
Now UserCreationParams gets generated the way you would expect.
The workaround only works when used once. If the Expand<T> is defined multiple times (across several files) it results in a 'Error: Multiple matching models found for referenced type Expand; please make model names unique'
I have not tried tagging one with the @tsoaModel annotation to see if that fixes it.
@RBornost defining Expand once and using it in multiple files should work AFAIK. Tagging one of the instances as @tsoaModel probably also works, although Expand should never end up in the swagger spec anyway, so that might be a bit misleading use of the decorator.
I don't think of Expand really as a type. It's a typescript trick to erase the connection between the base type and the derived type. There is also no connection between the derived type and Expand anymore.
I've run into this and found it annoying with none of the solutions working for me.
What worked for me is I found a little library openapi-flattener which you can use to post process your generated swagger and inline all schema refs.
It does however leave schemas in place, so I added some post-post-processing;
const specJson = JSON.parse(await fs.readFile(specFilePath, { encoding: "utf-8" }));
delete specJson.components.schemas;
await fs.writeFile(specFilePath, JSON.stringify(specJson));
+1 to flatten unneeded utility types or at least add a naming option