TypeScript遇Union type签名不兼容(2349):递归类型能否实现?
问题描述
目标API
我希望构建如下API:
export function GET({response}) { return response .match("text/html", "<p>Hello world</p>") .match("text/plain", "Hello world") .match("application/json", '{"message": "Hello world"}'); }
递归函数雏形
我已写出一个递归函数的雏形:
function makeMatch(list) { if(list.length === 0) { return "end" } return { match: item => makeMatch(list.filter(x => x !== item)) } }
使用方式如下:
const items = ["a", "b", "c"]; makeMatch(items).match("a").match("b").match("c"); // -> "end"
期望的TypeScript类型校验效果
我期望在TypeScript中实现以下类型校验效果:
makeMatch(items).match("a"); // ok makeMatch(items).match("x"); // 类型校验不通过,"x"不在["a", "b", "c"]中 makeMatch(items).match("a").match("b").match("a") // 类型校验不通过,"a"已经被使用过
当前实现代码
我已编写到如下阶段:
// makeMatch(emptyArray) returns "end" function makeMatch<T>(list: []): "end" // makeMatch(notEmptyArray) recurses function makeMatch<T>(list: T[]): T extends "never" ? "end" : { match: <Item extends T>(item: Item) => ReturnType<typeof makeMatch<Exclude<T, Item>>> } function makeMatch<T>(list: T[]) { if(list.length === 0) { return "end" } return { match: (item: T) => makeMatch(list.filter(x => x !== item)) } }
遇到的错误
但当我尝试使用该函数时:
const x = makeMatch<"json" | "text" | "csv">(["json", "text", "csv"])
出现了如下错误:
This expression is not callable. Each member of the union type `'(<Item extends "json">(item: Item) => Exclude<"json", Item> extends "never" ? "end" : { match: <Item extends Exclude<"json", Item>>(item: Item) => Exclude<Exclude<"json", Item>, Item> extends "never" ? "end" : { ...; }; }) | (<Item extends "text">(item: Item) => Exclude<...> extends "never" ? "end" : { ...; }) | (<I...'` has signatures, but none of those signatures are compatible with each other.(2349)
请问我想要实现的功能在TypeScript中是否可行?如果可行,该如何实现?
解决方案
这个功能在TypeScript中完全可行,问题出在当前类型定义使用了分布式条件类型,当T是联合类型时,会把match方法拆成多个函数的联合,而TypeScript无法调用签名不兼容的联合函数。
修正后的实现代码
// 递归定义匹配器类型 type Matcher<T extends string | number | symbol> = T extends never ? "end" : { match: <K extends T>(key: K) => Matcher<Exclude<T, K>> }; // 函数重载:空数组返回"end" function makeMatch<T extends string | number | symbol>(list: []): "end"; // 函数重载:非空数组返回对应Matcher function makeMatch<T extends string | number | symbol>(list: readonly T[]): Matcher<T>; // 核心实现 function makeMatch<T extends string | number | symbol>(list: readonly T[]) { if (list.length === 0) { return "end" as const; } return { match: (item: T) => { const remaining = list.filter(x => x !== item); return makeMatch(remaining) as Matcher<Exclude<T, typeof item>>; } }; }
关键调整说明
- 独立的
Matcher类型:把递归类型逻辑抽离成单独的Matcher,避免分布式条件类型触发拆分联合类型的问题,确保match方法始终是单一函数签名。 readonly数组参数:使用readonly T[]让TypeScript能更准确地推导数组元素的字面量类型(比如传入["a","b","c"]时,T会被推导为"a"|"b"|"c"而非string)。- 类型断言辅助推导:在
match方法返回时用类型断言,帮助TypeScript正确识别剩余元素的类型集合。
验证效果
const items = ["a", "b", "c"] as const; const matcher = makeMatch(items); matcher.match("a"); // ✅ 类型校验通过 matcher.match("x"); // ❌ 类型错误:"x"不属于"a"|"b"|"c" matcher.match("a").match("b").match("a"); // ❌ 类型错误:"a"已被排除,不在剩余集合中 matcher.match("a").match("b").match("c"); // ✅ 返回"end",类型校验通过
这样就能完全实现你想要的类型约束效果,同时解决了联合函数无法调用的错误。
内容的提问来源于stack exchange,提问作者Patrick McElhaney
相关产品推荐
相关产品推荐

