Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested generic with mapped types do not work #1268

Open
2 of 4 tasks
TimoGlastra opened this issue Jun 18, 2022 · 4 comments
Open
2 of 4 tasks

Nested generic with mapped types do not work #1268

TimoGlastra opened this issue Jun 18, 2022 · 4 comments

Comments

@TimoGlastra
Copy link

When using nested generics with mapped types, tsoa can't correctly infer the type of the nested payloads. If a layer of passing generics is removed, it will work.

Sorting

  • I'm submitting a ...

    • bug report
    • feature request
    • support request
  • I confirm that I

    • used the search to make sure that a similar issue hasn't already been submit

Expected Behavior

I would expect tsoa to follow multiple layers of nested/generic/mapped types. The interface with extra generics layer is semnatically the same as the one without, but produce different results.

Notice the precense of the PayloadMap_PayloadTypes_ in the json below

{
    "components": {
        "examples": {},
        "headers": {},
        "parameters": {},
        "requestBodies": {},
        "responses": {},
        "schemas": {
            "PayloadMap_PayloadTypes_": {
                "properties": {
                    "theKey": {
                        "properties": {
                            "thePayload": {
                                "type": "string"
                            }
                        },
                        "required": [
                            "thePayload"
                        ],
                        "type": "object"
                    }
                },
                "type": "object"
            },
            "TheRequestBody": {
                "properties": {
                    "payloadData": {
                        "$ref": "#/components/schemas/PayloadMap_PayloadTypes_"
                    }
                },
                "required": [
                    "payloadData"
                ],
                "type": "object",
                "additionalProperties": false
            }
        },
        "securitySchemes": {}
    },
    "info": {
        "title": "tsoa",
        "version": "1.0.0",
        "license": {
            "name": "MIT"
        },
        "contact": {}
    },
    "openapi": "3.0.0",
    "paths": {
        "/example": {
            "post": {
                "operationId": "Method",
                "responses": {
                    "204": {
                        "description": "No content"
                    }
                },
                "security": [],
                "parameters": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/TheRequestBody"
                            }
                        }
                    }
                }
            }
        }
    },
    "servers": [
        {
            "url": "/"
        }
    ]
}

I would expect to use the TheRequestBody with the generic.

import { Body, Controller, Post, Route } from "tsoa";

interface Payload {
  key: string;
  payload: unknown;
}

export interface ThePayload extends Payload {
  key: "theKey";
  payload: {
    thePayload: string;
  };
}

type PayloadMap<Payloads extends Payload[]> = {
  [Payload in Payloads[number] as Payload["key"]]?: Payload["payload"];
};

type PayloadTypes = [ThePayload];

// To make this work, remove the `Payloads` generic and pass `PayloadTypes` directly
// to the `PayloadMap` generic type.
export interface TheRequestBody<Payloads extends Payload[]> {
  payloadData: PayloadMap<Payloads>;
}

@Route("example")
export class ExampleController extends Controller {
  @Post()
  public async method(
    @Body()
    requestBody: TheRequestBody<PayloadTypes>
  ) {
    console.log(requestBody);
  }
}

Current Behavior

Notice the missing of the PayloadMap_PayloadTypes_ properties.

{
    "components": {
        "examples": {},
        "headers": {},
        "parameters": {},
        "requestBodies": {},
        "responses": {},
        "schemas": {
            "PayloadMap_PayloadTypes_": {
                "properties": {},
                "type": "object"
            },
            "TheRequestBody_PayloadTypes_": {
                "properties": {
                    "payloadData": {
                        "$ref": "#/components/schemas/PayloadMap_PayloadTypes_"
                    }
                },
                "required": [
                    "payloadData"
                ],
                "type": "object",
                "additionalProperties": false
            }
        },
        "securitySchemes": {}
    },
    "info": {
        "title": "tsoa",
        "version": "1.0.0",
        "license": {
            "name": "MIT"
        },
        "contact": {}
    },
    "openapi": "3.0.0",
    "paths": {
        "/example": {
            "post": {
                "operationId": "Method",
                "responses": {
                    "204": {
                        "description": "No content"
                    }
                },
                "security": [],
                "parameters": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/TheRequestBody_PayloadTypes_"
                            }
                        }
                    }
                }
            }
        }
    },
    "servers": [
        {
            "url": "/"
        }
    ]
}

To make it work I had to pass the PayloadTypes directly inside the TheRequestBody interface, instead of being able to pass it as a generic (as I used in the expected behaviour above).

import { Body, Controller, Post, Route } from "tsoa";

interface Payload {
  key: string;
  payload: unknown;
}

export interface ThePayload extends Payload {
  key: "theKey";
  payload: {
    thePayload: string;
  };
}

type PayloadMap<Payloads extends Payload[]> = {
  [Payload in Payloads[number] as Payload["key"]]?: Payload["payload"];
};

type PayloadTypes = [ThePayload];

export interface TheRequestBody {
  payloadData: PayloadMap<PayloadTypes>;
}

@Route("example")
export class ExampleController extends Controller {
  @Post()
  public async method(
    @Body()
    requestBody: TheRequestBody
  ) {
    console.log(requestBody);
  }
}

Possible Solution

Not sure how this works under the hood so can't provide a solutins, but it seems to me it has something to do with nesting of generics that is not being inferred properly

Steps to Reproduce

https://github.com/TimoGlastra/tsoa-error/tree/repro-nested-generics-mapped-types (note not main branch)

  1. yarn install, yarn build
  2. See swagger.json with missing type for PayloadMap_PayloadTypes_
  3. Update exampleController with changes from actual behaviour code (so directly pass PayloadTypes in TheRequestBody
  4. See swagger.json with correct type for PayloadMap_PayloadTypes_

Context (Environment)

Version of the library: 4.1.0
Version of NodeJS: 16.13.0

  • Confirm you were using yarn not npm: [x]

Detailed Description

Breaking change?

@github-actions
Copy link

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

@github-actions github-actions bot added the Stale label Jul 19, 2022
@TimoGlastra
Copy link
Author

Still relevant

@github-actions github-actions bot removed the Stale label Jul 20, 2022
@shashank42
Copy link

I also have the same issue.

@WoH
Copy link
Collaborator

WoH commented Jul 28, 2022

Please feel free to send a PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants