tsoa icon indicating copy to clipboard operation
tsoa copied to clipboard

Feature: inline type aliases (@tsoaInline)

Open jeremyVignelles opened this issue 4 years ago • 5 comments

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

jeremyVignelles avatar Feb 12 '21 19:02 jeremyVignelles

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.

vincentvanderweele avatar Feb 19 '21 08:02 vincentvanderweele

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 avatar Mar 12 '21 09:03 RBornost

@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.

vincentvanderweele avatar Apr 15 '21 15:04 vincentvanderweele

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));

gwatson1 avatar Apr 21 '22 12:04 gwatson1

+1 to flatten unneeded utility types or at least add a naming option

afarago avatar Nov 20 '22 14:11 afarago