Repository: lukeautry/tsoa
Pull request #509 (2019-10-10 22:24:56)

TypeResolver: Add defaults for type parameters to context
This is a backport from #505 because it's possible to have and review this on it's own.

```ts
interface MyModel<T = number> {
  numberOrT?: T
}

interface ReturnedFromController {
  model: MyModel
}
```
Now works, assuming your default is something we can resolve.

### 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?
* [ ] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [ ] This PR is associated with an existing issue?

**Closing issues**

Put `closes #XXXX`  (where XXXX is the issue number) in your comment to auto-close the issue that your PR fixes.

Haven't seen any issues related to this specifically.

### 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?

I haven't written negative cases because that would mean there's a model without a default that doesn't get any arguments and TypeScript ensures that code wouldn't even compile.


Patch:
diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts
index 87b5d041..fb30c000 100644
--- a/src/metadataGeneration/typeResolver.ts
+++ b/src/metadataGeneration/typeResolver.ts
@@ -191,9 +191,7 @@ export class TypeResolver {
       return literalType;
     }
 
-    if (typeReference.typeArguments && typeReference.typeArguments.length > 0) {
-      this.typeArgumentsToContext(typeReference, typeReference.typeName, this.context);
-    }
+    this.typeArgumentsToContext(typeReference, typeReference.typeName, this.context);
 
     const enumOrReferenceType = this.getReferenceTypeOrEnumType(typeReference.typeName as ts.EntityName, this.extractEnum, typeReference.typeArguments);
 
@@ -797,34 +795,37 @@ export class TypeResolver {
   private typeArgumentsToContext(type: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, targetEntitiy: ts.EntityName, context: Context): Context {
     this.context = {};
 
-    if (type.typeArguments && type.typeArguments.length > 0) {
-      const typeParameters = this.getModelTypeDeclaration(targetEntitiy).typeParameters;
-
-      if (typeParameters) {
-        for (let index = 0; index < typeParameters.length; index++) {
-          const typeParameter = typeParameters[index];
-          const typeArg = type.typeArguments[index];
-          let resolvedType: ts.TypeNode;
-
-          // Argument may be a forward reference from context
-          if (ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName) && context[typeArg.typeName.text]) {
-            resolvedType = context[typeParameter.name.text];
-          } else {
-            resolvedType = type.typeArguments[index];
-          }
-
-          this.context = {
-            ...this.context,
-            [typeParameter.name.text]: resolvedType,
-          };
+    const typeParameters = this.getModelTypeDeclaration(targetEntitiy).typeParameters;
+
+    if (typeParameters) {
+      for (let index = 0; index < typeParameters.length; index++) {
+        const typeParameter = typeParameters[index];
+        const typeArg = type.typeArguments && type.typeArguments[index];
+        let resolvedType: ts.TypeNode;
+
+        // Argument may be a forward reference from context
+        if (typeArg && ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName) && context[typeArg.typeName.text]) {
+          resolvedType = context[typeArg.typeName.text];
+        } else if (typeArg) {
+          resolvedType = typeArg;
+        } else if (typeParameter.default) {
+          resolvedType = typeParameter.default;
+        } else {
+          throw new GenerateMetadataError(`Could not find a value for type parameter ${typeParameter.name.text}`, type);
         }
+
+        this.context = {
+          ...this.context,
+          [typeParameter.name.text]: resolvedType,
+        };
       }
     }
     return context;
   }
 
   private getModelInheritedProperties(modelTypeDeclaration: Exclude<UsableDeclaration, ts.PropertySignature>): Tsoa.Property[] {
-    const properties = [] as Tsoa.Property[];
+    const properties: Tsoa.Property[] = [];
+
     if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) {
       return [];
     }
@@ -842,10 +843,7 @@ export class TypeResolver {
         const baseEntityName = t.expression as ts.EntityName;
 
         // create subContext
-        let resetCtx = this.context;
-        if (t.typeArguments && t.typeArguments.length > 0) {
-          resetCtx = this.typeArgumentsToContext(t, baseEntityName, this.context);
-        }
+        const resetCtx = this.typeArgumentsToContext(t, baseEntityName, this.context);
 
         const referenceType = this.getReferenceTypeOrEnumType(baseEntityName);
         if (referenceType) {


Test patch:
diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts
index 2135f8ea..ba0d643c 100644
--- a/tests/fixtures/testModel.ts
+++ b/tests/fixtures/testModel.ts
@@ -92,6 +92,8 @@ export interface TestModel extends Model {
       };
     };
   };
+
+  defaultGenericModel?: GenericModel;
 }
 
 export interface TypeAliasModel1 {
@@ -449,7 +451,7 @@ export class TestClassModel extends TestClassBaseModel {
   }
 }
 
-export interface GenericModel<T> {
+export interface GenericModel<T = string> {
   result: T;
   union?: T | string;
   nested?: GenericRequest<T>;
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index b62ca5b1..ecc7bd49 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -436,6 +436,13 @@ describe('Definition generation', () => {
           genericNestedArrayCharacter2: (propertyName, propertySchema) => {
             expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestTypeAliasModel2Array', `for property ${propertyName}.$ref`);
           },
+          defaultGenericModel: (propertyName, propertySchema) => {
+            expect(propertySchema.$ref).to.eq('#/definitions/GenericModel', `for property ${propertyName}.$ref`);
+
+            const definition = getValidatedDefinition('GenericModel', currentSpec);
+            expect(definition.properties!.result.type).to.deep.equal('string');
+            expect(definition.properties!.nested.$ref).to.deep.equal('#/definitions/GenericRequeststring');
+          },
           and: (propertyName, propertySchema) => {
             expect(propertySchema.type).to.eq('object', `for property ${propertyName}`);
             expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`);
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index de262a9e..039a47f2 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -506,6 +506,13 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
           genericNestedArrayCharacter2: (propertyName, propertySchema) => {
             expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestTypeAliasModel2Array', `for property ${propertyName}.$ref`);
           },
+          defaultGenericModel: (propertyName, propertySchema) => {
+            expect(propertySchema.$ref).to.eq('#/components/schemas/GenericModel', `for property ${propertyName}.$ref`);
+
+            const definition = getComponentSchema('GenericModel', currentSpec);
+            expect(definition.properties!.result.type).to.deep.equal('string');
+            expect(definition.properties!.nested.$ref).to.deep.equal('#/components/schemas/GenericRequeststring');
+          },
           and: (propertyName, propertySchema) => {
             expect(propertySchema).to.deep.include(
               {