TypeScript:使用可区分联合的函数类型给出错误

问题描述

在我的文件中,我有以下可区分的联合类型

interface SlackStageQuery {
  stage: 'slack';
}

interface GithubStageQuery {
  stage: 'github';
  id: string;
}

interface SuccessstageQuery {
  stage: 'success';
  name: string;
  link: string;
}

type StageQuery = SlackStageQuery | GithubStageQuery | SuccessstageQuery;

另外,我有以下界面:

interface Details<S extends OnboardingStage> {
  position: number;
  title: S extends 'success' ? (name: string) => string : string;
  subtitle: string;
  button: (query: StageQuery & { stage: S }) => JSX.Element;
}

然后,我有一个符合 ONBOARDING_STAGE_DETAILS 的任意对象 { [p in OnboardingStage]: Details<p> }

我还有一个 query 类型的变量 StageQuery

要从 button 的子对象之一获取 ONBOARDING_STAGE_DETAILS 属性,我这样做

const foo = ONBOARDING_STAGE_DETAILS[query.stage].button(query)

然而,由于某种原因,我收到了这种类型的错误

TS2345: Argument of type 'StageQuery' is not assignable to parameter of type 'never'.   Type 'SlackStageQuery' is not assignable to type 'never'.

我不知道为什么会这样,因为例如,如果 query.stage === 'slack',那么 ONBOARDING_STAGE_DETAILS[query.stage] 将是 Details<'slack'> 类型,其 button 属性采用 { {1}},它正在提供。所以我认为所有类型都可以工作。

有谁知道这是为什么,我该如何解决?谢谢!

解决方法

我无法完全解释类型错误,但我感觉好像是因为query在编译时是联合类型,那么button(query) {}方法中参数的推断类型也将是一个联合,不知何故/某处与 { stage : } 的交集无法解析,因此 never 类型已解析。


在这里大声思考...

declare const query: StageQuery;
const details = ONBOARDING_STAGE_DETAILS[query.stage];    // (1)

details.button(query);    // (2)

(1): querySlackStageQuery | GithubStageQuery | SuccessStageQuery 类型。 query.stageOnboardingStage 类型,它是 "success" | "slack" | "github" 的联合类型。 所以 detailsDetails<S> 类型,其中 S = OnboardingStage

(2):扩展details.button()的参数类型:

StageQuery & { stage: S }
=> StageQuery & { stage: "success" | "slack" | "github" }
=> (SlackStageQuery | GithubStageQuery | SuccessStageQuery) & { stage: "success" | "slack" | "github" }
=> (SlackStageQuery & { stage: "success" | "slack" | "github" }) | ...
=> /* typescript internal type resolution */
=> never(?!)

如果其他人能弄清楚这如何解析为 never,或者这是否是调试类型解析的正确方法,我也很乐意学习:)


至于如何修复,您可以利用key remapping重新定义您的界面以完全避免交叉类型:

interface Details<Query extends StageQuery> {
               // ^~~~~~~~~~~~~~~~~~note the change here
  position: number;
  title: Query["stage"] extends 'success' ? (name: string) => string : string;
  subtitle: string;
  button: (query: Query) => any;
}

type OnboardingStage = StageQuery["stage"];

declare const ONBOARDING_STAGE_DETAILS: {
    [query in StageQuery as StageQuery["stage"]]: Details<query>
    // ^~~~~ map over the StageQueries,but the object is indexed by StageQuery["stage"]
}

declare const query: StageQuery;
ONBOARDING_STAGE_DETAILS[query.stage].button(query)  // OK! :)

const ghdetails: Details<GithubStageQuery> = {
  position: 1,title: "",subtitle: "",button: (query) => { /*query inferred as GitHubQuery */}
}

Playground link

,

由于您已经在使用联合类型 (StageQuery),因此您不需要还使用交集(每个 StageQuery 类型的变量都已经有一个 stage 属性)所以你可以简单地删除它,代码就可以工作了:

interface Details<S extends OnboardingStage> {
  position: number;
  title: S extends 'success' ? (name: string) => string : string;
  subtitle: string;
  button: (query: StageQuery) => any;
}

游乐场链接:https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgMoBtEGtVjgcwgEUBXaAT2QG8AoZZAZz0IC5kByBzBLdgbhoBfGjVCRYiFAHFgYABYkARrgLEyUSrXpNVbdvlkLF-OsmAATNkyih8A4aPDR4SNCQRIGDFYVIVqpjqsHAzungwm9CBwALYQVmA2IHam6KBYCUkpDmDkAA4oPmr+ALxo3DjMxRrIAD7IMvJKRX419ahhEF4t6uQCNLkFyADyIIoA9nBQ5rZFyGWcFex1HAZNxiucnV4mjuIuKAAiEHjA6AwAPKjIEAAekCDmDCNjk9OzVQB8AfR54wyyYDjEBsEAkGKKaACehgWToeJoG73CCPZ5bDxdCLIAD8yAAFNE4plbABKebfay2ZDE5LQxhKWFgeE0lL0RQkMBgYFsPEAR16bB6FDJJW+cBAfSEInMEAQmCgKAQwKYyH5FEFVVakpoMrlU0VyrAIwAcgAhYYAQQASocAJLGqQAfVQABULVIAKKOw4et22gAyqDYVGQAG0AApmEAvCZTGbJIoAXTYx1O5wu4e+gn6SpAKpg43G8xN5utdodzrdnu9votAdQobVGgAdEEIInm+zOcC+b0SXwgA