Repository: lukeautry/tsoa (ts)

Issue:
Error: Unknown type: TypeOperator
I'm having an issue when trying to define a type with keys of an enum. 

## 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

```ts
enum Enum {
  OK,
  KO
}
export interface SerializedDatasource {
  enumKeys: keyof typeof Enum // which produces 'OK' | 'KO'
}
```

```yaml
enumKeys:
  type: string
  enum:
    - OK
    - KO
```

## Current Behavior

```
There was a problem resolving type of 'SerializedDatasource'.
Generate swagger error.
 Error: Unknown type: TypeOperator
At: src/controllers/test.ts:11:11.
This was caused by 'enumKeys: keyof typeof Enum'
    at new GenerateMetadataError (/private/tmp/tsoa-bug/packages/api/node_modules/tsoa/dist/metadataGeneration/exceptions.js:21:28)
    at TypeResolver.resolve (/private/tmp/tsoa-bug/packages/api/node_modules/tsoa/dist/metadataGeneration/typeResolver.js:243:19)
    at TypeResolver.propertyFromSignature (/private/tmp/tsoa-bug/packages/api/node_modules/tsoa/dist/metadataGeneration/typeResolver.js:645:119)
    at /private/tmp/tsoa-bug/packages/api/node_modules/tsoa/dist/metadataGeneration/typeResolver.js:612:161
    at Array.map (<anonymous>)
```

## Context (Environment)
Version of the library: "3.0.4"
Version of NodeJS: v12.14.1
* Confirm you were using yarn not npm: [x]


Pull request:
feat: handle `typeof`
### 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 #632 


Patch:
diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts
index e9e86a6a..025ee951 100644
--- a/src/metadataGeneration/typeResolver.ts
+++ b/src/metadataGeneration/typeResolver.ts
@@ -207,6 +207,16 @@ export class TypeResolver {
       }
     }
 
+    if (ts.isTypeOperatorNode(this.typeNode) && this.typeNode.operator === ts.SyntaxKind.KeyOfKeyword) {
+      const type = this.current.typeChecker.getTypeFromTypeNode(this.typeNode);
+      try {
+        return new TypeResolver(this.current.typeChecker.typeToTypeNode(type)!, this.current, this.typeNode, this.context, this.referencer).resolve();
+      } catch (err) {
+        const indexedTypeName = this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.type));
+        throw new GenerateMetadataError(`Could not determine the keys on ${indexedTypeName}`, this.typeNode);
+      }
+    }
+
     if (this.typeNode.kind !== ts.SyntaxKind.TypeReference) {
       throw new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode);
     }


Test patch:
diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts
index 686caa91..ac00006f 100644
--- a/tests/fixtures/testModel.ts
+++ b/tests/fixtures/testModel.ts
@@ -564,12 +564,19 @@ export class PrivateModel {
   }
 }
 
+enum MyEnum {
+  OK,
+  KO,
+}
+
 /**
  * This is a description of TestClassModel
  */
 export class TestClassModel extends TestClassBaseModel {
   public account: Account;
   public defaultValue2 = 'Default Value 2';
+  public enumKeys: keyof typeof MyEnum;
+  public keyInterface?: keyof Model;
   /**
    * This is a description of a public string property
    *
diff --git a/tests/integration/dynamic-controllers-express-server.spec.ts b/tests/integration/dynamic-controllers-express-server.spec.ts
index 1e64e504..f2130db0 100644
--- a/tests/integration/dynamic-controllers-express-server.spec.ts
+++ b/tests/integration/dynamic-controllers-express-server.spec.ts
@@ -1153,6 +1153,7 @@ describe('Express Server', () => {
     model.publicStringProperty = 'test';
     model.stringProperty = 'test';
     model.account = { id: 1234 };
+    model.enumKeys = 'OK';
 
     return model;
   }
diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts
index 10f99d80..85d74c45 100644
--- a/tests/integration/express-server.spec.ts
+++ b/tests/integration/express-server.spec.ts
@@ -1243,6 +1243,7 @@ describe('Express Server', () => {
     model.publicStringProperty = 'test';
     model.stringProperty = 'test';
     model.account = { id: 1234 };
+    model.enumKeys = 'OK';
 
     return model;
   }
diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts
index 6c723c16..9b72acc7 100644
--- a/tests/integration/hapi-server.spec.ts
+++ b/tests/integration/hapi-server.spec.ts
@@ -1082,6 +1082,7 @@ describe('Hapi Server', () => {
     model.publicStringProperty = 'test';
     model.stringProperty = 'test';
     model.account = { id: 1234 };
+    model.enumKeys = 'OK';
 
     return model;
   }
diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts
index 6fd616d2..157e9ed2 100644
--- a/tests/integration/koa-server.spec.ts
+++ b/tests/integration/koa-server.spec.ts
@@ -1067,6 +1067,7 @@ describe('Koa Server', () => {
     model.publicStringProperty = 'test';
     model.stringProperty = 'test';
     model.account = { id: 1234 };
+    model.enumKeys = 'KO';
 
     return model;
   }
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index b425854b..12ac980f 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -756,6 +756,8 @@ describe('Definition generation', () => {
                 type: 'string',
                 enum: [
                   'id',
+                  'enumKeys',
+                  'keyInterface',
                   'publicStringProperty',
                   'optionalPublicStringProperty',
                   'emailPattern',
@@ -826,7 +828,9 @@ describe('Definition generation', () => {
               {
                 properties: {
                   defaultValue1: { type: 'string', default: 'Default Value 1', description: undefined, format: undefined, example: undefined },
+                  enumKeys: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined, enum: ['OK', 'KO'], 'x-nullable': false },
                   id: { type: 'number', format: 'double', default: undefined, description: undefined, example: undefined },
+                  keyInterface: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined, 'x-nullable': false, enum: ['id'] },
                   optionalPublicConstructorVar: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined },
                   readonlyConstructorArgument: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined },
                   publicConstructorVar: { type: 'string', default: undefined, description: 'This is a description for publicConstructorVar', format: undefined, example: undefined },
@@ -846,7 +850,7 @@ describe('Definition generation', () => {
                   defaultValue2: { type: 'string', default: 'Default Value 2', description: undefined, format: undefined, example: undefined },
                   account: { $ref: '#/definitions/Account', format: undefined, description: undefined, example: undefined },
                 },
-                required: ['account', 'publicStringProperty', 'stringProperty', 'publicConstructorVar', 'readonlyConstructorArgument', 'id'],
+                required: ['account', 'enumKeys', 'publicStringProperty', 'stringProperty', 'publicConstructorVar', 'readonlyConstructorArgument', 'id'],
                 type: 'object',
                 description: 'Make all properties in T readonly',
                 default: undefined,
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index f5099fbc..73accf36 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -921,6 +921,8 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
               {
                 oneOf: [
                   { type: 'string', enum: ['id'], nullable: false },
+                  { type: 'string', enum: ['enumKeys'], nullable: false },
+                  { type: 'string', enum: ['keyInterface'], nullable: false },
                   { type: 'string', enum: ['publicStringProperty'], nullable: false },
                   { type: 'string', enum: ['optionalPublicStringProperty'], nullable: false },
                   { type: 'string', enum: ['emailPattern'], nullable: false },
@@ -990,7 +992,15 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
               {
                 properties: {
                   defaultValue1: { type: 'string', default: 'Default Value 1', description: undefined, format: undefined, example: undefined },
+                  enumKeys: {
+                    default: undefined,
+                    description: undefined,
+                    format: undefined,
+                    example: undefined,
+                    oneOf: [{ enum: ['OK'], nullable: false, type: 'string' }, { enum: ['KO'], nullable: false, type: 'string' }],
+                  },
                   id: { type: 'number', format: 'double', default: undefined, description: undefined, example: undefined },
+                  keyInterface: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined, enum: ['id'], nullable: false },
                   optionalPublicConstructorVar: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined },
                   readonlyConstructorArgument: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined },
                   publicConstructorVar: { type: 'string', default: undefined, description: 'This is a description for publicConstructorVar', format: undefined, example: undefined },
@@ -1010,7 +1020,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
                   defaultValue2: { type: 'string', default: 'Default Value 2', description: undefined, format: undefined, example: undefined },
                   account: { $ref: '#/components/schemas/Account', format: undefined, description: undefined, example: undefined },
                 },
-                required: ['account', 'publicStringProperty', 'stringProperty', 'publicConstructorVar', 'readonlyConstructorArgument', 'id'],
+                required: ['account', 'enumKeys', 'publicStringProperty', 'stringProperty', 'publicConstructorVar', 'readonlyConstructorArgument', 'id'],
                 type: 'object',
                 description: 'Make all properties in T readonly',
                 default: undefined,