小能豆

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

javascript

我正在尝试将一个对象传递给一个函数(在本例中是传递给 React 组件的 props)。

该对象包含以下属性:

  • 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) => unknownrender数据类型为ReturnType<S>
  • 使用单独的类型来推断和提取的selector返回类型type Extract<D, S> = S extends (data: D) => infer T ? T : D

阅读 41

收藏
2024-06-10

共1个答案

小能豆

TS4.7+ 的答案

microsoft/TypeScript#48538似乎将与 TypeScript 4.7 一起发布,届时,具有方法的对象文字将能够通过从先前成员推断出的任何泛型类型参数按上下文类型输入其参数。这意味着您想要的代码将正常工作:

Component({
    data: { test: true },
    selector: (data) => data.test,
    render: (test) => test, // test is boolean, HOORAY! 🎉
})

请注意,这对对象文字成员的顺序很敏感,因此您不能在不破坏事物的情况下切换顺序:

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

TS4.7+ 的游乐场链接


TS4.6 的先前答案-

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

问题在于,您给出了selectorrender属性回调,其参数(datatest)没有显式类型注释。因此,它们是上下文类型的;编译器需要推断这些参数的类型,而不能直接使用它们来推断其他类型。编译器会推迟这一操作,并尝试根据当前所知来推断D和。它可以推断为,因为属性属于这种类型。但是它不知道要推断为什么,所以它默认为。此时,编译器可以开始执行上下文类型:现在已知回调的参数是类型,但回调的参数被赋予了类型。此时,类型推断结束,您陷入困境。S``D``{test: boolean}``data``S``unknown``data``selector``{test: boolean}``test``render``unknown

根据TypeScript 首席架构师的评论:

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


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

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 的类型推断功能不足时,我倾向于使用构建器模式,但这取决于你。

2024-06-10