tsoa
tsoa copied to clipboard
Nested generic with mapped types do not work
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 ...
- [x] bug report
- [ ] feature request
- [ ] support request
-
I confirm that I
- [x] 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)
- yarn install, yarn build
- See swagger.json with missing type for
PayloadMap_PayloadTypes_
- Update exampleController with changes from actual behaviour code (so directly pass
PayloadTypes
inTheRequestBody
- 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?
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
Still relevant
I also have the same issue.
Please feel free to send a PR