类型属性依赖于另一个属性的返回类型

IT技术 reactjs typescript
2021-04-17 10:13:38

我正在尝试将一个对象传递给一个函数(在这种情况下,props 传递给一个 React 组件)。

该对象包含以下属性:

  • data - 一些任意数据
  • selector - 将返回部分数据的函数
  • render - 处理渲染选定数据的函数 (JSX)

我不确定如何正确输入。

我最初认为它可以通过这种方式完成:

type Props<D, S> = {
  data: D
  selector: (data: D) => S
  render: (data: S) => any
}

const Component = <D, S>(props: Props<D, S>) => null

Component({
  data: { test: true },
  selector: (data) => data.test,
  render: (test) => test, // test is unknown
})

这导致通用 ,S未知。但是,如果我们删除render依赖属性,S我们会得到正确的返回类型(布尔值)。

我也试过:

  • 使用泛型参数S extends (data: D) => unknown,用render的数据类型为ReturnType<S>
  • 使用单独的类型来推断和提取selector的返回类型type Extract<D, S> = S extends (data: D) => infer T ? T : D
2个回答

您遇到了 TypeScript 的设计限制。有关更多信息,请参阅microsoft/TypeScript#38872

问题是您提供了selectorrender属性回调,其参数 (datatest) 没有明确的类型注释。因此它们是上下文类型的编译器需要为这些参数推断类型,不能直接使用它们来推断其他类型。编译器拥有过就这个问题和试图推断D,并S从它目前知道。它可以推断D{test: boolean}因为该data属性属于这种类型。但它不知道推断什么S,它默认为unknown此时,编译器可以开始执行上下文类型:在data该参数selector的回调,现在是已知的类型{test: boolean},但test的参数render回调被赋予类型unknown在这一点上,类型推断结束,你被卡住了。

根据TypeScript 首席架构师的评论

为了支持这一特定场景,我们需要额外的推理阶段,即每个上下文敏感属性值一个,类似于我们对多个上下文敏感参数所做的。不清楚我们想在那里冒险,并且它仍然对编写对象文字成员的顺序很敏感。最终,如果类型推断没有完全统一,我们可以做的事情是有限的。


那么可以做什么呢?问题在于上下文回调参数类型和通用参数推断之间的相互作用。如果你愿意放弃其中之一,你可以切断类型推断的 Gordian Knot。例如,您可以通过手动注释一些回调参数类型来放弃一些上下文回调参数类型:

Component({
  data: { test: true },
  selector: (data: { test: boolean }) => data.test, // annotate here
  render: (test) => test, // test is boolean
})

或者,您可以通过手动指定泛型类型参数来放弃泛型参数类型推断:

Component<{ test: boolean }, boolean>({ // specify here
  data: { test: true },
  selector: (data) => data.test,
  render: (test) => test, // test is boolean
})

或者,如果您不愿意这样做,也许您可​​以Props<D, S>分阶段创建您的值,其中每个阶段只需要一点点类型推断。例如,您可以用函数参数替换属性值(请参阅上面的引用“类似于我们对多个上下文敏感参数所做的操作”):

const makeProps = <D, S>(
  data: D, selector: (data: D) => S, render: (data: S) => any
): Props<D, S> => ({ data, selector, render });

Component(makeProps(
  { test: true },
  (data) => data.test,
  (test) => test // boolean
));

或者,更详细但可能更容易理解,使用流畅的 构建器模式:

const PropsBuilder = {
  data: <D,>(data: D) => ({
    selector: <S,>(selector: (data: D) => S) => ({
      render: (render: (data: S) => any): Props<D, S> => ({
        data, selector, render
      })
    })
  })
}

Component(PropsBuilder
  .data({ test: true })
  .selector((data) => data.test)
  .render((test) => test) // boolean
);

在 TypeScript 的类型推断功能不足的情况下,我倾向于更喜欢构建器模式,但这取决于您。

Playground 链接到代码

感谢您提供信息丰富的答案!我认为可能是这种情况。一旦我回去工作,我将不得不考虑你的第三个建议。
2021-06-01 10:13:38
尽管您正在寻找格式,但构建器模式在 React props 中效果不佳 <Component data={{ test: true }} selector={(data) => data.test} render={(test) => test} />
2021-06-02 10:13:38

在这里你有一个解决方案:

import React from 'react'

type Props<D, S> = {
  data: D
  selector: (data: D) => S
  render: (data: S) => any
}


const Comp = <D, S>(props: Props<D, S>) => null

const result = <Comp<number, string> data={2} selector={(data: number) => 'fg'} render={(data: string) => 42} /> // ok
const result2 = <Comp<number, string> data={2} selector={(data: string) => 'fg'} render={(data: string) => 42} /> // expected error
const result3 = <Comp<number, string> data={2} selector={(data: number) => 'fg'} render={(data: number) => 42} /> // expected error

您应该为组件显式定义泛型

我应该补充一点,我的目标是完全通过推理来做到这一点,因为我使用的实际类型有点超出我的掌握(生成的 GraphQL 查询类型)。尝试为其中之一指定选择器返回类型可能有点麻烦。
2021-06-01 10:13:38