为何Express中泛型内外联合类型定义响应体的表现不同?
我尝试给Express请求处理器定义类型,确保响应体是两种类型之一,但发现两种类型定义方式的类型检查结果差异很大:
interface Foo { a: number; } interface Bar { b: string; } let foo = { a: 42 }; let bar = { b: "abc" }; let foobar = { ...foo, ...bar }; // 写法1:泛型参数内使用联合类型 app.get("/foobar1", async (req, res: Response<Foo | Bar>) => { // 全部通过类型检查 res.json(foo); res.json(bar); res.json(foobar); }); // 写法2:泛型参数外使用联合类型 app.get("/foobar2", async (req, res: Response<Foo> | Response<Bar>) => { // 前两行报错,仅最后一行通过 res.json(foo); res.json(bar); res.json(foobar); });
当联合类型写在泛型参数内部时,类型检查符合预期——只要响应体是Foo或Bar即可通过。但写在泛型参数外部时,只有同时包含Foo和Bar所有属性的对象才能通过检查,仿佛响应体类型被转换成了Foo & Bar。
奇怪的是,这种情况在普通泛型接口里不会出现:
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 }; // 所有赋值都通过编译 let baaz1: Outside = { data: foo }; let baaz2: Inside = { data: foo }; let baaz3: Outside = { data: foobar }; let baaz4: Inside = { data: foobar };
我知道可以用第一种写法解决问题,但想弄清楚:为什么Response<Foo | Bar>和Response<Foo> | Response<Bar>的类型检查行为会有这么大差异?
核心原因是TypeScript中函数参数的逆变特性,以及Express的Response类型的结构设计。
1. 两种类型的本质区别
Response<Foo | Bar>:这是一个单一类型,它的json方法参数被定义为Foo | Bar——只要传入的对象符合其中任意一种类型即可。Response<Foo> | Response<Bar>:这是两个不同Response类型的联合,其中Response<Foo>的json方法只接受Foo类型,Response<Bar>的json方法只接受Bar类型。
2. 函数联合类型的逆变规则
当你调用一个联合类型的函数时,TypeScript会要求传入的参数必须同时满足联合中所有函数的参数要求——也就是参数类型会变成所有参数类型的交集(Foo & Bar)。
这是因为TypeScript无法确定当前的res到底是Response<Foo>还是Response<Bar>:
- 如果传入
foo(仅符合Foo),万一是Response<Bar>的话,调用json(foo)就会违反Bar的类型约束; - 如果传入
bar(仅符合Bar),万一是Response<Foo>的话,同样会违反类型约束; - 只有传入同时符合
Foo和Bar的foobar,才能在两种分支下都合法。
3. 普通泛型接口为何没有这个问题
你定义的Wrapper是包含属性的接口,属性属于协变位置:当你有Wrapper<Foo> | Wrapper<Bar>时,它的data属性类型是Foo | Bar,而非交集。因为属性的读取操作是安全的——不管是Foo还是Bar,读取data时都能拿到对应类型的值,不需要强制满足所有分支的约束。
而Response的json是方法(函数),函数参数属于逆变位置,所以会触发交集类型的约束。
内容的提问来源于stack exchange,提问作者Galen Long

