Tsoa: Feature: inline type aliases (@tsoaInline)

Created on 12 Feb 2021  路  3Comments  路  Source: lukeautry/tsoa

Sorting

  • I'm submitting a ...

    • [ ] bug report
    • [X] feature request
    • [ ] support request
  • I confirm that I

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

I did, and I found a lot of other similar issues, for example :

  • #798
  • #751

Repro project

https://github.com/jeremyVignelles/repro-tsoa/tree/repro/pick-partial-omit

Expected Behavior


Expected swagger.json

{
    "components": {
        "examples": {},
        "headers": {},
        "parameters": {},
        "requestBodies": {},
        "responses": {},
        "schemas": {
            "User": {
                "properties": {
                    "id": {
                        "type": "number",
                        "format": "double"
                    },
                    "email": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "Happy",
                            "Sad"
                        ]
                    },
                    "phoneNumbers": {
                        "items": {
                            "type": "string"
                        },
                        "type": "array"
                    }
                },
                "required": [
                    "id",
                    "email",
                    "name",
                    "phoneNumbers"
                ],
                "type": "object",
                "additionalProperties": false
            },
            "UserCreationParams": {
                "properties": {
                    "email": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "phoneNumbers": {
                        "items": {
                            "type": "string"
                        },
                        "type": "array"
                    }
                },
                "required": [
                    "email",
                    "name",
                    "phoneNumbers"
                ],
                "type": "object"
            },
            "UserPatchParams": {
                "properties": {
                    "email": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "phoneNumbers": {
                        "items": {
                            "type": "string"
                        },
                        "type": "array"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "Happy",
                            "Sad"
                        ]
                    }
                },
                "type": "object"
            }
        },
        "securitySchemes": {}
    },
    "info": {
        "title": "repro-tsoa",
        "version": "1.0.0",
        "license": {
            "name": "MIT"
        },
        "contact": {
            "name": "J茅r茅my VIGNELLES ",
            "email": "[email protected]"
        }
    },
    "openapi": "3.0.0",
    "paths": {
        "/users/{userId}": {
            "get": {
                "operationId": "GetUser",
                "responses": {
                    "200": {
                        "description": "Ok",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/User"
                                }
                            }
                        }
                    }
                },
                "security": [],
                "parameters": [
                    {
                        "in": "path",
                        "name": "userId",
                        "required": true,
                        "schema": {
                            "format": "double",
                            "type": "number"
                        }
                    },
                    {
                        "in": "query",
                        "name": "name",
                        "required": false,
                        "schema": {
                            "type": "string"
                        }
                    }
                ]
            },
            "patch": {
                "operationId": "PatchUser",
                "responses": {
                    "204": {
                        "description": "No content"
                    }
                },
                "security": [],
                "parameters": [
                    {
                        "in": "path",
                        "name": "userId",
                        "required": true,
                        "schema": {
                            "format": "double",
                            "type": "number"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/UserPatchParams"
                            }
                        }
                    }
                }
            }
        },
        "/users": {
            "post": {
                "operationId": "CreateUser",
                "responses": {
                    "201": {
                        "description": "Created"
                    }
                },
                "security": [],
                "parameters": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/UserCreationParams"
                            }
                        }
                    }
                }
            }
        }
    },
    "servers": [
        {
            "url": "/"
        }
    ]
}

/**
 * @tsoaInline
 */
export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;

/**
 * @tsoaInline
 */
export type UserPatchParams = Partial<Omit<User, "id">>

Current Behavior


Generated swagger.json

{
    "components": {
        "examples": {},
        "headers": {},
        "parameters": {},
        "requestBodies": {},
        "responses": {},
        "schemas": {
            "User": {
                "properties": {
                    "id": {
                        "type": "number",
                        "format": "double"
                    },
                    "email": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "Happy",
                            "Sad"
                        ]
                    },
                    "phoneNumbers": {
                        "items": {
                            "type": "string"
                        },
                        "type": "array"
                    }
                },
                "required": [
                    "id",
                    "email",
                    "name",
                    "phoneNumbers"
                ],
                "type": "object",
                "additionalProperties": false
            },
            "Pick_User.email-or-name-or-phoneNumbers_": {
                "properties": {
                    "email": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "phoneNumbers": {
                        "items": {
                            "type": "string"
                        },
                        "type": "array"
                    }
                },
                "required": [
                    "email",
                    "name",
                    "phoneNumbers"
                ],
                "type": "object",
                "description": "From T, pick a set of properties whose keys are in the union K"
            },
            "UserCreationParams": {
                "$ref": "#/components/schemas/Pick_User.email-or-name-or-phoneNumbers_"
            },
            "Partial_Omit_User.id__": {
                "properties": {
                    "email": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "phoneNumbers": {
                        "items": {
                            "type": "string"
                        },
                        "type": "array"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "Happy",
                            "Sad"
                        ]
                    }
                },
                "type": "object",
                "description": "Make all properties in T optional"
            },
            "UserPatchParams": {
                "$ref": "#/components/schemas/Partial_Omit_User.id__"
            }
        },
        "securitySchemes": {}
    },
    "info": {
        "title": "repro-tsoa",
        "version": "1.0.0",
        "license": {
            "name": "MIT"
        },
        "contact": {
            "name": "J茅r茅my VIGNELLES ",
            "email": "[email protected]"
        }
    },
    "openapi": "3.0.0",
    "paths": {
        "/users/{userId}": {
            "get": {
                "operationId": "GetUser",
                "responses": {
                    "200": {
                        "description": "Ok",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/User"
                                }
                            }
                        }
                    }
                },
                "security": [],
                "parameters": [
                    {
                        "in": "path",
                        "name": "userId",
                        "required": true,
                        "schema": {
                            "format": "double",
                            "type": "number"
                        }
                    },
                    {
                        "in": "query",
                        "name": "name",
                        "required": false,
                        "schema": {
                            "type": "string"
                        }
                    }
                ]
            },
            "patch": {
                "operationId": "PatchUser",
                "responses": {
                    "204": {
                        "description": "No content"
                    }
                },
                "security": [],
                "parameters": [
                    {
                        "in": "path",
                        "name": "userId",
                        "required": true,
                        "schema": {
                            "format": "double",
                            "type": "number"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/UserPatchParams"
                            }
                        }
                    }
                }
            }
        },
        "/users": {
            "post": {
                "operationId": "CreateUser",
                "responses": {
                    "201": {
                        "description": "Created"
                    }
                },
                "security": [],
                "parameters": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/UserCreationParams"
                            }
                        }
                    }
                }
            }
        }
    },
    "servers": [
        {
            "url": "/"
        }
    ]
}

Note that the type alias are generated as

    "UserCreationParams": {
        "$ref": "#/components/schemas/Pick_User.email-or-name-or-phoneNumbers_"
    },

which could be simplified as client code generators (like NSwag) are unable to handle this properly.

Possible Solution

As mentionned above, there could be a JSDoc annotation that would allow to inline the type alias to flatten its type aliases indirections.

Steps to Reproduce

Context (Environment)

Version of the library: 3.5.2
Version of NodeJS: 14

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

Detailed Description

The jsdoc annotation can be placed on a type declaration, like so:

export interface User {
  id: number;
  email: string;
  name: string;
  status?: "Happy" | "Sad";
  phoneNumbers: string[];
}

/**
 * A post request should not contain an id.
 * @tsoaInline
 */
export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;

If both types are used, the above code would generate the same thing as if they were in fact two separate interfaces, that is, the same as :

export interface User {
  id: number;
  email: string;
  name: string;
  status?: "Happy" | "Sad";
  phoneNumbers: string[];
}

/**
 * A post request should not contain an id.
 */
export interface UserCreationParams {
  email: string;
  name: string;
  phoneNumbers: string[];
}

Breaking change?

No, the new syntax would be opt-in

help wanted workaround available

Most helpful comment

There is a workaround for this, although not pretty. You first need to make sure that you only use interfaces on your API, so replace

export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;

by

export interface UserCreationParams extends Pick<User, "email" | "name" | "phoneNumbers"> {};

Then, you need to flatten the base type on the right, for which you can use a helper type like this:

type Expand<T> = { [K in keyof T]: T[K] };

export interface UserCreationParams extends Expand<Pick<User, "email" | "name" | "phoneNumbers">> {};

Now UserCreationParams gets generated the way you would expect.

All 3 comments

There is a workaround for this, although not pretty. You first need to make sure that you only use interfaces on your API, so replace

export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;

by

export interface UserCreationParams extends Pick<User, "email" | "name" | "phoneNumbers"> {};

Then, you need to flatten the base type on the right, for which you can use a helper type like this:

type Expand<T> = { [K in keyof T]: T[K] };

export interface UserCreationParams extends Expand<Pick<User, "email" | "name" | "phoneNumbers">> {};

Now UserCreationParams gets generated the way you would expect.

The workaround only works when used once. If the Expand is defined multiple times (across several files) it results in a
'Error: Multiple matching models found for referenced type Expand; please make model names unique'

I have not tried tagging one with the @tsoaModel annotation to see if that fixes it.

@RBornost defining Expand once and using it in multiple files should work AFAIK. Tagging one of the instances as @tsoaModel probably also works, although Expand should never end up in the swagger spec anyway, so that might be a bit misleading use of the decorator.

I don't think of Expand really as a type. It's a typescript trick to erase the connection between the base type and the derived type. There is also no connection between the derived type and Expand anymore.

Was this page helpful?
0 / 5 - 0 ratings