Repository: lukeautry/tsoa (ts)

Issue:
Aliased void Api Response types use 200 response instead of 204
## Sorting

* **I'm submitting a ...**
  - [x] bug report
  - [ ] feature request
  - [ ] support request

* I confirm that I
  - [x] used the [search](https://github.com/lukeautry/tsoa/search?type=Issues) to make sure that a similar issue hasn't already been submit

## Expected Behavior
<!--- Tell us what should happen -->
<!--- If this is related to swagger/OpenAPI files, then please include swagger.yaml that you wish tsoa had generated  -->
```yaml
     "/assets/v1/{id}": {
      "delete": {
        "operationId": "deleteAsset",
        "responses": {
          "204": {
            "content": {
              "application/json": {}
            },
            "description": "No content"
          },
```
<!--- If this is related to a TypeScript issue, then include the way that you expected to be able to write the code  -->
```ts

    type AssetDeleteResponseType = void;

    public async delete(
        id: string,
        @Request() request: express.Request,
    ): Promise<AssetDeleteResponse> 
```

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
<!--- If this is related to swagger/OpenAPI files, then please include swagger.yaml that tsoa actually generated  -->
```yaml
{
  "components": {
    "examples": {},
    "headers": {},
    "parameters": {},
    "requestBodies": {},
    "responses": {},
    "schemas": {
      "AssetDeleteResponse": {},
    },
    "paths": {
     "/assets/v1/{id}": {
      "delete": {
        "operationId": "deleteAsset",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetDeleteResponse"
                }
              }
            },
            "description": "Ok"
          },
```
<!--- If this is related to a TypeScript issue, then include the way that you had to write the code  -->
```ts
    public async delete(
        id: string,
        @Request() request: express.Request,
    ): Promise<void> {
```

## Possible Solution
It seems like the only solution to this would be to detect that the type resolves to void and change the open api spec to have the response under 204.  This means that the type alias doesn't carry over to any remote clients however at least its accurate.

### Workaround
Use void directly in api responses that return void.  This isn't a bad workaround but I lost some time to the unexpected behavior so I decided to go ahead and file it.

## Context (Environment)
Version of the library: 3.0.2
Version of NodeJS: 12.12.0
* Confirm you were using yarn not npm: [x]


Pull request:
Aliased void Api Response types document 200 response instead of 204
### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/lukeautry/tsoa/tree/master/docs/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/lukeautry/tsoa/pulls) for the same update/change?
* [x] Have you written unit tests?
* [x] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [x] This PR is associated with an existing issue?

**Closing issues**

Closes #603 

### If this is a new feature submission:

* [x] Has the issue had a maintainer respond to the issue and clarify that the feature is something that aligns with the [goals](https://github.com/lukeautry/tsoa#goal) and [philosophy](https://github.com/lukeautry/tsoa#philosophy) of the project?

**Potential Problems With The Approach**
None I'm aware of

Patch:
diff --git a/src/metadataGeneration/methodGenerator.ts b/src/metadataGeneration/methodGenerator.ts
index 6f47ba18..16e43681 100644
--- a/src/metadataGeneration/methodGenerator.ts
+++ b/src/metadataGeneration/methodGenerator.ts
@@ -1,4 +1,5 @@
 import * as ts from 'typescript';
+import { isVoidType } from '../utils/isVoidType';
 import { getDecorators } from './../utils/decoratorUtils';
 import { getJSDocComment, getJSDocDescription, isExistJSDocTag } from './../utils/jsDocUtils';
 import { GenerateMetadataError } from './exceptions';
@@ -144,11 +145,12 @@ export class MethodGenerator {
 
   private getMethodSuccessResponse(type: Tsoa.Type): Tsoa.Response {
     const decorators = this.getDecoratorsByIdentifier(this.node, 'SuccessResponse');
+
     if (!decorators || !decorators.length) {
       return {
-        description: type.dataType === 'void' ? 'No content' : 'Ok',
+        description: isVoidType(type) ? 'No content' : 'Ok',
         examples: this.getMethodSuccessExamples(),
-        name: type.dataType === 'void' ? '204' : '200',
+        name: isVoidType(type) ? '204' : '200',
         schema: type,
       };
     }
diff --git a/src/swagger/specGenerator2.ts b/src/swagger/specGenerator2.ts
index 6263996a..aa5935ba 100644
--- a/src/swagger/specGenerator2.ts
+++ b/src/swagger/specGenerator2.ts
@@ -1,5 +1,6 @@
 import { Tsoa } from '../metadataGeneration/tsoa';
 import { assertNever } from '../utils/assertNever';
+import { isVoidType } from '../utils/isVoidType';
 import { SwaggerConfig } from './../config';
 import { convertColonPathParams, normalisePath } from './../utils/pathUtils';
 import { SpecGenerator } from './specGenerator';
@@ -179,7 +180,7 @@ export class SpecGenerator2 extends SpecGenerator {
       swaggerResponses[res.name] = {
         description: res.description,
       };
-      if (res.schema && res.schema.dataType !== 'void') {
+      if (res.schema && !isVoidType(res.schema)) {
         swaggerResponses[res.name].schema = this.getSwaggerType(res.schema);
       }
       if (res.examples) {
diff --git a/src/swagger/specGenerator3.ts b/src/swagger/specGenerator3.ts
index 3bccab16..8a8bde3e 100644
--- a/src/swagger/specGenerator3.ts
+++ b/src/swagger/specGenerator3.ts
@@ -1,5 +1,6 @@
 import { Tsoa } from '../metadataGeneration/tsoa';
 import { assertNever } from '../utils/assertNever';
+import { isVoidType } from '../utils/isVoidType';
 import { SwaggerConfig } from './../config';
 import { convertColonPathParams, normalisePath } from './../utils/pathUtils';
 import { SpecGenerator } from './specGenerator';
@@ -268,7 +269,7 @@ export class SpecGenerator3 extends SpecGenerator {
         },
         description: res.description,
       };
-      if (res.schema && res.schema.dataType !== 'void') {
+      if (res.schema && !isVoidType(res.schema)) {
         /* tslint:disable:no-string-literal */
         (swaggerResponses[res.name].content || {})['application/json']['schema'] = this.getSwaggerType(res.schema);
       }
diff --git a/src/utils/isVoidType.ts b/src/utils/isVoidType.ts
new file mode 100644
index 00000000..386a07b6
--- /dev/null
+++ b/src/utils/isVoidType.ts
@@ -0,0 +1,11 @@
+import { Tsoa } from '../metadataGeneration/tsoa';
+
+export const isVoidType = (type: Tsoa.Type): boolean => {
+  if (type.dataType === 'void') {
+    return true;
+  } else if (type.dataType === 'refAlias') {
+    return isVoidType(type.type);
+  } else {
+    return false;
+  }
+};


Test patch:
diff --git a/tests/fixtures/controllers/methodController.ts b/tests/fixtures/controllers/methodController.ts
index 87fc73ea..d74fc7cb 100644
--- a/tests/fixtures/controllers/methodController.ts
+++ b/tests/fixtures/controllers/methodController.ts
@@ -106,4 +106,10 @@ export class MethodController extends Controller {
   public async returnAnyType(): Promise<any> {
     return 'Hello Word';
   }
+
+  @Get('returnAliasedVoidType')
+  public async returnAliasedVoidType(): Promise<VoidAlias1> {}
 }
+
+type VoidAlias1 = VoidAlias2;
+type VoidAlias2 = void;
diff --git a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
index b136b0a8..646b15c5 100644
--- a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
@@ -46,6 +46,7 @@ describe('Metadata generation', () => {
       'summaryMethod',
       'oauthAndAPIkeySecurity',
       'returnAnyType',
+      'returnAliasedVoidType',
     ];
 
     it('should only generate the defined methods', () => {
@@ -163,6 +164,19 @@ describe('Metadata generation', () => {
       expect(mainResponse.description).to.equal('Created');
     });
 
+    it('should generate 204 response on aliased voids', () => {
+      const method = controller.methods.find(m => m.name === 'returnAliasedVoidType');
+      if (!method) {
+        throw new Error('Method returnAliasedVoidType not defined!');
+      }
+
+      expect(method.responses.length).to.equal(1);
+
+      const mainResponse = method.responses[0];
+      expect(mainResponse.name).to.equal('204');
+      expect(mainResponse.description).to.equal('No content');
+    });
+
     it('should generate api security', () => {
       const method = controller.methods.find(m => m.name === 'apiSecurity');
       if (!method) {