You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>>;
    }
  };
}

关键调整说明

  1. 独立的Matcher类型:把递归类型逻辑抽离成单独的Matcher,避免分布式条件类型触发拆分联合类型的问题,确保match方法始终是单一函数签名。
  2. readonly数组参数:使用readonly T[]让TypeScript能更准确地推导数组元素的字面量类型(比如传入["a","b","c"]时,T会被推导为"a"|"b"|"c"而非string)。
  3. 类型断言辅助推导:在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 23:19:01