TypeScript参数类型推断失败

10

我为此创建了错误报告,但也许有人有解决方法。基本上,我希望根据另一个类型有条件地向函数传递一些参数:


declare function foo<P>(params: { f: (p: P) => void } & P): void;

foo({
  f(params: { bar(x: number): void }) {},
  bar(x) {},
});

因此,有一个特殊的参数f,它具有函数类型,无论该函数期望什么属性,都必须作为参数包含在foo中。这里再次提到,对于bar的参数被推断为any但应该是number

2个回答

4
每当我看到需要依赖于参数值的函数类型时,"使用重载"是我首先想到的东西:
declare function foo(params: { tag: 'a', bar(x: number): void }): void;
declare function foo(params: { tag: 'b', bar(x: boolean): void }): void;

foo({ tag: 'a', bar(x) { console.log('x =', x)} }); // (parameter) x: number

更新:我假设在下面的代码中,foof是React组件,并且foof作为它的属性公开,以及f拥有的所有属性。
declare function foo<P>(params: { f: (p: P) => void } & P): void;

foo({
  f(params: { bar(x: number): void }) {},
  bar(x) {},
});

有一种方法可以重写它,如果你不坚持在调用时内联声明foo的属性f的名称,以及f属性的类型。

您可以声明一个类型,描述这种组合组件的属性,它需要两个泛型参数:内部组件属性的类型和外部属性中内部组件的名称:

type ComposedProps<InnerProps, N extends string> = 
       InnerProps & { [n in N]: (props: InnerProps) => void };  

// Also I assume in reality it does not return `void` but `React.Element`,
//  but I don't think it matters

declare function foo<P>(params: P): void;

// foo generic parameter is given explicitly,
//  which allows to infer (and not repeat) parameter types for `f` and `bar` 
foo<ComposedProps<{bar(x: number): void}, 'f'>>({
  f(params) {}, // params: { bar(x: number): void; }
  bar(x) {}, // (parameter) x: number
});

我已经编辑了我的问题,添加了一个更实际的示例,其中重载集将是无限的。 - Tom Crockett
@TomCrockett,你修改后的版本存在问题,因为对象字面类型引用了自身,编译器在推断这样的递归结构时有限制(至少我所知道的是如此)。这是你真正想要建模的东西吗?还是这也是一个更或多或少准确的近似值(即使是小差异也可能导致可能和不可能之间的差异,你已经到达了类型系统的极限 :))? - Titian Cernicova-Dragomir
@TomCrockett 我不能说我有使用过MaterialUI,所以这对我来说有点陌生。在这样的方法中,可能有一些关于this类型的事情我们可以做。例如:`function foo<T>(foo: T & ThisType<T & { default : number }>) { }foo({ customValue: 10, method() { this.customValue + this.default } })`但是没有一个最小的工作示例,我无法确定这是否有帮助。 - Titian Cernicova-Dragomir
1
@TitianCernicova-Dragomir 如果你感兴趣,这里有一个更详细的PR,介绍了真实用例的更多细节:https://github.com/mui-org/material-ui/pull/11731 - Tom Crockett
1
@artem 感谢您的更新,是的 foof 是 React 组件。我们真的希望避免为外部组件提供类型参数,而是让它推断内部组件的类型,从而推断出其他属性的类型。还有一个复杂的问题,即 component 属性(例如此示例中的 f)是可选的,并且应该默认为某个具有特定一组附加属性的组件。例如,在 Button 的情况下,当未提供 component 时,默认为 button,这意味着可以使用所有 button 的标准属性。 - Tom Crockett
显示剩余2条评论

2

正如我们在评论中讨论的那样,这种情况实际上与React组件有关,以及新的通用组件功能如何与默认的通用参数交互。

问题

如果我们有一个带有通用参数的React组件,并且通用参数的默认值具有函数字段,则我们传递给函数的参数将无法推断。一个简单的例子是:

  class Foo<P = { fn : (e: string)=> void }> extends React.Component<P>{}
  let foo = () => (
    <div>
      <Foo fn={e=> e}/> {/* e is any */}
    </div>
  )

可能的解决方案

首先声明一下,我不知道这个方法在所有情况下是否适用,或者它是否依赖于某些编译器行为的更改。但我发现这是一种非常脆弱的解决方案,任何一个细节变化,即使是某些被认为是等价的内容,都会导致它无法工作。但根据我的测试,就目前呈现出的情况而言,它可以正常工作。

这个解决方案是利用属性类型来自构造函数返回值的事实,如果我们将默认泛型参数简化一点,就可以得到我们想要的类型推断。唯一的问题是如何检测到我们正在处理默认泛型参数。我们可以通过为默认参数添加一个额外的字段,并使用条件类型进行测试来解决这个问题。还有一个需要注意的地方,这对于可以基于已使用属性推断P的简单情况不起作用,因此我们将提供一个从另一个组件中提取属性的例子(实际上更接近于您的用例):

// Do not change { __default?:true } to DefaultGenericParameter it only works with the former for some reason
type IfDefaultParameter<C, Default, NonDefault> = C extends { __default?:true } ? Default : NonDefault
type DefaultGenericParameter = { __default? : true; }

export type InferredProps<C extends React.ReactType> =
  C extends React.ComponentType<infer P> ? P :
  C extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[C] :
  {};

// The props for Foo, will extract props from the generic parameter passed in 
type FooProps<T extends React.ReactType = React.ComponentClass<{ fn: (s: string )=> void }>> = InferredProps<T> & {
  other?: string
}

// Actual component class
class _Foo<P> extends React.Component<P>{}

// The public facing constructor 
const Foo : {
  new <P extends React.ReactType & {__default?: true} = React.ComponentClass<{ fn: (s: string )=> void }> & DefaultGenericParameter>(p: P) :
  IfDefaultParameter<P, _Foo<FooProps>, _Foo<FooProps<P>>>
} = _Foo as any;

  let foo = () => (
    <div>
      <Foo fn={e=> e}/> {/* e is string */}
      <Foo<React.ComponentClass<{ fn: (s: number )=> void }>> fn={e=> e}/> {/* e is number */}
    </div>
  )

应用于 Material UI

我在 Material UI 仓库中尝试过的一个示例是 Avatar 组件,它似乎可以正常工作:

export type AvatarProps<C extends AnyComponent = AnyComponent> = StandardProps<
  PassthruProps<C, 'div'>,
  AvatarClassKey
> & {
  alt?: string;
  childrenClassName?: string;
  component?: C;
  imgProps?: React.HtmlHTMLAttributes<HTMLImageElement>;
  sizes?: string;
  src?: string;
  srcSet?: string;
};

type IfDefaultParameter<C, Default, NonDefault> = C extends { __default?:true } ? Default : NonDefault
type DefaultGenericParameter = { __default? : true; }

declare const Avatar : {
  new <C extends AnyComponent & DefaultGenericParameter = 'div' & DefaultGenericParameter>(props: C) 
  : IfDefaultParameter<C, React.Component<AvatarProps<'div'>>, React.Component<AvatarProps<C>>>
}

使用方法

const AvatarTest = () => (
  <div>
    {/* e is React.MouseEvent<HTMLDivElement>*/}
    <Avatar onClick={e => log(e)} alt="Image Alt" src="example.jpg" />
    {/* e is {/* e is React.MouseEvent<HTMLButtonElement>*/}*/}
    <Avatar<'button'> onClick={e => log(e)} component="button" alt="Image Alt" src="example.jpg" />
    <Avatar<React.SFC<{ x: number }>>
      component={props => <div>{props.x}</div>} // props is props: {x: number;} & {children?: React.ReactNode;}
      x={3} // ok 
      // IS NOT allowed:
      // onClick={e => log(e)}
      alt="Image Alt"
      src="example.jpg"
    />
  </div>

祝你好运,如果有帮助请告诉我。这是一个有趣的小问题,现在已经让我失眠了10天 :)


这看起来很有前途,因为它适用于默认情况,并允许在覆盖情况下明确指定正确的props。不幸的是,如果存在 component prop,则额外props的推断类型似乎不再与 component prop一致。例如,在 <Avatar onClick={e => log(e)} component="button" />(未提供泛型)中,e 的类型被错误地推断为 React.MouseEvent<HTMLDivElement>。只要记住如果提供了 component prop,则必须提供泛型,它似乎就能工作。 - Tom Crockett
赏金即将到期,这是我看到的最好的答案,所以我会把它给你 :) 感谢你的帮助! - Tom Crockett
是的,你说得对。当你指定按钮但不指定类型参数时,应该至少出现错误提示。我会看看明天能否让它按照这种方式工作。 - Titian Cernicova-Dragomir
@TomCrockett 将默认的 props 更改为 React.Component<AvatarProps<'div'>>,而不是 React.Component<AvatarProps>,现在如果 component 是没有类型参数设置的,则会出现错误。这种不一致性是由于 AvatarProps 中指定了 div 的默认值而不是由于此解决方案引起的。 - Titian Cernicova-Dragomir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接