I want to type an Express request handler to enforce that the response body has one of two types. While doing this, I discovered that Express interprets the following ways of typing the response body differently:
interface Foo {
a: number;
}
interface Bar {
b: string;
}
let foo = { a: 42 };
let bar = { b: "abc" };
let foobar = { ...foo, ...bar };
// union inside generic
app.get("/foobar1", async (req, res: Response<Foo | Bar>) => {
// all of these type check
res.json(foo);
res.json(bar);
res.json(foobar);
});
// union outside generic
app.get("/foobar2", async (req, res: Response<Foo> | Response<Bar>) => {
// these first two don't type check
res.json(foo);
res.json(bar);
// only this type checks
res.json(foobar);
});
When the union is inside the generic argument, the type checking behaves as I expect - if I send a response body that contains either of my body types, it compiles. But when the union is outside the generic argument, Express converts the body type into Foo & Bar
, so only sending a Foo or only sending a Bar gets a type error.
This seems to be an Express-specific thing, because if I try to reproduce this on a simpler interface with a generic argument, I don't get any type errors:
interface Wrapper<T> {
data: T;
}
type Outside = Wrapper<Foo> | Wrapper<Bar>;
type Inside = Wrapper<Foo | Bar>;
let foo = { a: 42 };
let bar = { b: "abc" };
let foobar = { ...foo, ...bar };
// all of the below compiles
let baaz1: Outside = { data: foo };
let baaz2: Inside = { data: foo };
let baaz3: Outside = { data: foobar };
let baaz4: Inside = { data: foobar };
While I can simply put the union inside the generic argument, I was curious why this happens. Why does Response<Foo | Bar>
type response bodies as Foo | Bar
, but Response<Foo> | Response<Bar>
type responses bodies as Foo & Bar
?