Repository: asyncapi/modelina (ts)

Issue:
TypeScript generator should take additionalItems into consideration
#### Reason/Context
Even though we interpret additionalItems (#224) we do not take it into consideration when rendering the TS types for arrays. 

See https://github.com/asyncapi/modelina/issues/224#issuecomment-863200247 for further details on how it should be rendered.


Pull request:
feat: render tuple additionalItems for TS
**Description**
Whenever an array is defined as a tuple, additionalItems control if it is allowed to specify addition items that may be added to the tuples so it acts more like an array.

**Related issue(s)**
fixes #260 

Patch:
diff --git a/src/generators/typescript/TypeScriptRenderer.ts b/src/generators/typescript/TypeScriptRenderer.ts
index 4943b005..d083fe6d 100644
--- a/src/generators/typescript/TypeScriptRenderer.ts
+++ b/src/generators/typescript/TypeScriptRenderer.ts
@@ -54,8 +54,9 @@ export abstract class TypeScriptRenderer extends AbstractRenderer<TypeScriptOpti
         const types = model.items.map((item) => {
           return this.renderType(item);
         });
-        return `[${types.join(', ')}]`;
-      } 
+        const additionalTypes = model.additionalItems ? `, ...(${this.renderType(model.additionalItems)})[]` : '';
+        return `[${types.join(', ')}${additionalTypes}]`;
+      }
       const arrayType = model.items ? this.renderType(model.items) : 'unknown';
       return `Array<${arrayType}>`;
     }
diff --git a/src/models/CommonModel.ts b/src/models/CommonModel.ts
index 55523b81..4f01428c 100644
--- a/src/models/CommonModel.ts
+++ b/src/models/CommonModel.ts
@@ -336,7 +336,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
    * @param originalSchema 
    * @param alreadyIteratedModels
    */
-  private static mergeProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
+  private static mergeProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema | boolean, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
     const mergeToProperties = mergeTo.properties;
     const mergeFromProperties = mergeFrom.properties;
     if (mergeFromProperties !== undefined) {
@@ -362,7 +362,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
    * @param originalSchema 
    * @param alreadyIteratedModels
    */
-  private static mergeAdditionalProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
+  private static mergeAdditionalProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema | boolean, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
     const mergeToAdditionalProperties = mergeTo.additionalProperties;
     const mergeFromAdditionalProperties = mergeFrom.additionalProperties;
     if (mergeFromAdditionalProperties !== undefined) {
@@ -382,7 +382,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
    * @param originalSchema 
    * @param alreadyIteratedModels
    */
-  private static mergeAdditionalItems(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
+  private static mergeAdditionalItems(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema | boolean, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
     const mergeToAdditionalItems = mergeTo.additionalItems;
     const mergeFromAdditionalItems= mergeFrom.additionalItems;
     if (mergeFromAdditionalItems !== undefined) {
@@ -402,7 +402,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
    * @param originalSchema 
    * @param alreadyIteratedModels
    */
-  private static mergePatternProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
+  private static mergePatternProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema | boolean, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
     const mergeToPatternProperties = mergeTo.patternProperties;
     const mergeFromPatternProperties = mergeFrom.patternProperties;
     if (mergeFromPatternProperties !== undefined) {
@@ -430,7 +430,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
    * @param alreadyIteratedModels
    */
   // eslint-disable-next-line sonarjs/cognitive-complexity
-  private static mergeItems(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
+  private static mergeItems(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema | boolean, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
     if (mergeFrom.items === undefined) { return; }
     if (Array.isArray(mergeFrom.items) && mergeFrom.items.length === 0) { return; }
     if (mergeTo.items === undefined) {
@@ -496,7 +496,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
    * @param originalSchema 
    * @param alreadyIteratedModels
    */
-  static mergeCommonModels(mergeTo: CommonModel | undefined, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()): CommonModel {
+  static mergeCommonModels(mergeTo: CommonModel | undefined, mergeFrom: CommonModel, originalSchema: Schema | boolean, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()): CommonModel {
     if (mergeTo === undefined) {return mergeFrom;}
     Logger.debug(`Merging model ${mergeFrom.$id || 'unknown'} into ${mergeTo.$id || 'unknown'}`, mergeTo, mergeFrom, originalSchema);
     if (alreadyIteratedModels.has(mergeFrom)) {return alreadyIteratedModels.get(mergeFrom) as CommonModel;}


Test patch:
diff --git a/test/blackbox/docs/dummy.json b/test/blackbox/docs/dummy.json
index 649d070b..e83c07bb 100644
--- a/test/blackbox/docs/dummy.json
+++ b/test/blackbox/docs/dummy.json
@@ -40,6 +40,9 @@
     "dummyArrayWithArray": {
       "$ref": "#/definitions/dummyArrayWithArray"
     },
+    "dummyArrayWithArrayNoAdditionalItems": {
+      "$ref": "#/definitions/dummyArrayWithArrayNoAdditionalItems"
+    },
     "dummyObject": {
       "$ref": "#/definitions/dummyObject"
     }
@@ -82,7 +85,23 @@
         {
           "type": "number"
         }
-      ]
+      ],
+      "additionalItems": true
+    },
+    "dummyArrayWithArrayNoAdditionalItems": {
+      "type": "array",
+      "items": [
+        {
+          "$ref": "#/definitions/dummyInfo"
+        },
+        {
+          "type": "string"
+        },
+        {
+          "type": "number"
+        }
+      ],
+      "additionalItems": false
     },
     "dummyObject": {
       "type": "object",
diff --git a/test/generators/typescript/TypeScriptGenerator.spec.ts b/test/generators/typescript/TypeScriptGenerator.spec.ts
index cefc24b9..da821d4e 100644
--- a/test/generators/typescript/TypeScriptGenerator.spec.ts
+++ b/test/generators/typescript/TypeScriptGenerator.spec.ts
@@ -16,7 +16,8 @@ describe('TypeScriptGenerator', () => {
         house_number: { type: 'number' },
         marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
         members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
-        tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
+        tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }], additionalItems: false },
+        tuple_type_with_additional_items: { type: 'array', items: [{ type: 'string' }, { type: 'number' }], additionalItems: true },
         array_type: { type: 'array', items: { type: 'string' } },
       },
       required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
@@ -29,6 +30,7 @@ describe('TypeScriptGenerator', () => {
   private _marriage?: boolean;
   private _members?: string | number | boolean;
   private _tupleType?: [string, number];
+  private _tupleTypeWithAdditionalItems?: [string, number, ...(object | string | number | Array<unknown> | boolean | null | number)[]];
   private _arrayType: Array<string>;
   private _additionalProperties?: Map<String, object | string | number | Array<unknown> | boolean | null | number>;
 
@@ -40,6 +42,7 @@ describe('TypeScriptGenerator', () => {
     marriage?: boolean,
     members?: string | number | boolean,
     tupleType?: [string, number],
+    tupleTypeWithAdditionalItems?: [string, number, ...(object | string | number | Array<unknown> | boolean | null | number)[]],
     arrayType: Array<string>,
   }) {
     this._streetName = input.streetName;
@@ -49,6 +52,7 @@ describe('TypeScriptGenerator', () => {
     this._marriage = input.marriage;
     this._members = input.members;
     this._tupleType = input.tupleType;
+    this._tupleTypeWithAdditionalItems = input.tupleTypeWithAdditionalItems;
     this._arrayType = input.arrayType;
   }
 
@@ -73,6 +77,9 @@ describe('TypeScriptGenerator', () => {
   get tupleType(): [string, number] | undefined { return this._tupleType; }
   set tupleType(tupleType: [string, number] | undefined) { this._tupleType = tupleType; }
 
+  get tupleTypeWithAdditionalItems(): [string, number, ...(object | string | number | Array<unknown> | boolean | null | number)[]] | undefined { return this._tupleTypeWithAdditionalItems; }
+  set tupleTypeWithAdditionalItems(tupleTypeWithAdditionalItems: [string, number, ...(object | string | number | Array<unknown> | boolean | null | number)[]] | undefined) { this._tupleTypeWithAdditionalItems = tupleTypeWithAdditionalItems; }
+
   get arrayType(): Array<string> { return this._arrayType; }
   set arrayType(arrayType: Array<string>) { this._arrayType = arrayType; }
 
@@ -149,7 +156,8 @@ ${content}`;
         house_number: { type: 'number' },
         marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
         members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
-        tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
+        tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }], additionalItems: false },
+        tuple_type_with_additional_items: { type: 'array', items: [{ type: 'string' }, { type: 'number' }], additionalItems: true },
         array_type: { type: 'array', items: { type: 'string' } },
       },
       required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
@@ -162,6 +170,7 @@ ${content}`;
   marriage?: boolean;
   members?: string | number | boolean;
   tupleType?: [string, number];
+  tupleTypeWithAdditionalItems?: [string, number, ...(object | string | number | Array<unknown> | boolean | null | number)[]];
   arrayType: Array<string>;
   additionalProperties?: Map<String, object | string | number | Array<unknown> | boolean | null | number>;
 }`;
diff --git a/test/generators/typescript/TypeScriptRenderer.spec.ts b/test/generators/typescript/TypeScriptRenderer.spec.ts
index 76ce2021..fb656b2a 100644
--- a/test/generators/typescript/TypeScriptRenderer.spec.ts
+++ b/test/generators/typescript/TypeScriptRenderer.spec.ts
@@ -35,6 +35,12 @@ describe('TypeScriptRenderer', () => {
       model.items = [CommonModel.toCommonModel({type: 'number'})];
       expect(renderer.toTsType('array', model)).toEqual('[number]');
     });
+    test('Should render tuple type with additionalItems', () => {
+      const model = new CommonModel();
+      model.items = [CommonModel.toCommonModel({type: 'number'})];
+      model.additionalItems = CommonModel.toCommonModel({type: 'string'});
+      expect(renderer.toTsType('array', model)).toEqual('[number, ...(string)[]]');
+    });
     test('Should render multiple tuples', () => {
       const model = new CommonModel();
       model.items = [CommonModel.toCommonModel({type: 'number'}), CommonModel.toCommonModel({type: 'string'})];