tsoa icon indicating copy to clipboard operation
tsoa copied to clipboard

Generic with intersection: `Could not match intersection against any of the possible combinations: [[]]`

Open CodeSmith32 opened this issue 8 months ago • 1 comments

TSOA resolves the following type for Body correctly:

interface Foo {
  x: number;
  y: number;
}

@Route("/v1/foo")
class Foo extends Controller {
  @Post("/")
  async route(@Body() data: Partial<Foo> & Required<Pick<Foo, "x">>) {
    // ...
  }
}

But as soon as I try to make this type generic, TSOA breaks down:

interface Foo {
  x: number;
  y: number;
}

type OnlyRequire<T, U extends keyof T> = Partial<T> & Required<Pick<T, U>>;

type FooDto = OnlyRequire<Foo, "x">;

@Route("/v1/foo")
class Foo extends Controller {
  @Post("/")
  async route(@Body() data: FooDto) {
    // ...
  }
}

At this point, when the routes are generated, the validation model data appears something like follows, where the resulting type is a union of two objects with no properties in nestedProperties:

const models: TsoaRoute.Models = {
    "Partial_Foo_": {
        "dataType": "refAlias",
        "type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"validators":{}}, // <----- No properties
    },
    // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
    "Required_Pick_Foo.x__": {
        "dataType": "refAlias",
        "type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"validators":{}}, // <----- No properties
    },
    // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
    "OnlyRequire_Foo.x_": {
        "dataType": "refAlias",
        "type": {"dataType":"intersection","subSchemas":[{"ref":"Partial_Foo_"},{"ref":"Required_Pick_Foo.x__"}],"validators":{}},
    },
}

Thus, it doesn't crash when it runs, but any requests to this endpoint fail with a validation error:

Could not match intersection against any of the possible combinations: [[]]

CodeSmith32 avatar Apr 28 '25 16:04 CodeSmith32

Ok, I was not able to reproduce this issue on StackBlitz because, sometimes the TypeResolver gets used, and sometimes the type is directly resolved via the TypeScript lib. I have no idea when one happens over the other, making it extremely hard to trip the mechanism to break. TypeScript's built-in resolver pretty much always works. But TSOA's resolver has serious issues.. this is just another one.

With that said, if anyone knows some trick to trip up TSOA to use its custom resolver over the TypeScript resolver, I will try to exploit it and update the StackBlitz to reproduce the issue.


In any case, the issue is that TSOA's resolver doesn't handle mapped types properly. In this case, it fails to process TypeScript's lib definition of the Partial utility. The declaration is as such:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

The AST for this appears as such:

TypeAliasDeclaration {
  name: Identifier {
    escapedText: "Partial"
  }
  typeParameters: [
    TypeParameter {
      name: Identifier {
        escapedText: "T"
      }
    }
  ]
  type: MappedType {
    typeParameter: TypeParameter {
      name: Identifier {
        escapedText: "P"
      }
      constraint: TypeOperator {
        operator: keyof
        type: TypeReference {
          typeName: Identifier {
            escapedText: "T"
          }
        }
      }
    }
    questionToken: QuestionToken { }
    type: IndexedAccessType {
      objectType: TypeReference {
        typeName: Identifier {
          escapedText: "T"
        }
      }
      indexType: TypeReference {
        typeName: Identifier {
          escapedText: "P"
        }
      }
    }
    members: [ ] // <----
  }
}

From what I debugged, since the members array is empty, the call to getProperties() under the mapped type handler returns an empty array:

// .....
        } else if (this.hasFlag(type, ts.TypeFlags.Object)) {
          const typeProperties: ts.Symbol[] = type.getProperties(); // <-----
          const properties: Tsoa.Property[] = typeProperties
// .....

It does look like it tries to get the non-named 'index' signatures after this too, but it's definitely returning an empty array (maybe a TypeScript bug?):

Image

Let me know if more info could be helpful.

CodeSmith32 avatar Apr 29 '25 18:04 CodeSmith32

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[bot] avatar Sep 27 '25 00:09 github-actions[bot]

Closed? Well, whatever. We have better alternatives to tsoa, like trpc. In our case, we're using a custom system with Zod-based validation that works incredibly well. We won't be using tsoa again, so I guess it makes no difference for us if this is fixed or not.

Unfortunately, tsoa has always been very unstable, likely due to the constantly changing nature of typescript's type system. I would not recommend tsoa for production use.

CodeSmith32 avatar Oct 27 '25 16:10 CodeSmith32