TypeScript 参数类型推断失败

Posted

技术标签:

【中文标题】TypeScript 参数类型推断失败【英文标题】:TypeScript parameter type inference failure 【发布时间】:2018-12-13 11:01:11 【问题描述】:

我为此创建了一个bug report,但也许有人有解决方法的想法。基本上我想根据另一个函数的类型有条件地为函数提供一些参数:

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

【问题讨论】:

【参考方案1】:

每当我看到需要依赖于参数值的函数类型时,首先想到的是“使用重载”:

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 组件,并且 foo 公开了 f 作为它的属性,以及 @987654328 的所有属性@有。

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

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

有一种方法可以重写它,你不坚持f的名称(这是foo的属性),以及f属性的类型必须在@内内联声明987654333@ 调用时的参数。

你可以声明一个类型来描述这种组合组件的属性,它有两个泛型参数:内部组件属性的类型,以及外部属性中内部组件的名称:

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

【讨论】:

我已经编辑了我的问题以添加一个更现实的示例,其中重载集将是无限的。 @TomCrockett 您编辑的版本的问题是对象文字类型引用了自身并且编译器在推断这种递归结构方面有限制(至少据我所知)这是真的吗您要建模的东西?或者这也是一个或多或少精确的近似值(即使是微小的差异也可能导致可能和不可能之间的差异,你处于类型系统的外部限制:)) @TitianCernicova-Dragomir 是的,这更接近真正的问题。如果您有兴趣,真正的问题是为 Material UI 定义准确的类型,其中许多组件遵循允许 component 覆盖道具的模式,外部组件上的任何“额外”道具都将传播到该道具。参见例如material-ui.com/api/button。我意识到这可能超出了类型系统可以处理的范围,但认为问一下也无妨:) @TitianCernicova-Dragomir 如果您好奇的话,有一个 PR 提供有关实际用例的更多详细信息:github.com/mui-org/material-ui/pull/11731 @artem 感谢您的更新,是的,foof 是反应组件。我们真的希望避免为外部组件提供类型参数,而是让它推断内部组件的类型,从而推断附加道具的类型。还有一个复杂的问题是component 属性(即本例中的f)是可选的,并且应该默认为具有一组特定附加属性的某些 特定 组件。例如。对于Button,当没有提供component时默认为button,这意味着可以使用button的所有标准props【参考方案2】:

正如我们在 cmets 中讨论的那样,该场景实际上与 React 组件以及新的泛型组件功能与默认泛型参数交互的方式有关。

问题

如果我们有一个带有泛型参数的 React 组件,并且泛型参数的默认值具有 function 字段,则不会推断我们传入的函数的参数。一个简单的例子是:

  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 存储库中尝试过的一个示例是 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)  src="example.jpg" />
    /* e is /* e is React.MouseEvent<HTMLButtonElement>*/*/
    <Avatar<'button'> onClick=e => log(e) component="button"  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)
      
      src="example.jpg"
    />
  </div>

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

【讨论】:

这看起来很有希望,因为它适用于默认情况,并且它允许在覆盖情况下明确指定正确的道具。不幸的是,额外道具的推断类型似乎与component 道具(如果存在)不再一致。例如在&lt;Avatar onClick=e =&gt; log(e) component="button" /&gt;(未提供泛型)中,e 的类型被错误地推断为React.MouseEvent&lt;HTMLDivElement&gt;。只要您记住,如果您提供 component 属性,则必须提供泛型,它似乎有效。 赏金即将到期,这是我见过的最好的答案,所以我要把它给你:) 谢谢你的帮助! 是的,您是对的,您指定按钮但未指定类型参数的情况至少应该是一个错误。我明天看看能不能让它这样工作。 @TomCrockett 将默认道具更改为 React.Component&lt;AvatarProps&lt;'div'&gt;&gt; 而不是 React.Component&lt;AvatarProps&gt; 现在如果 component 是在没有类型参数的情况下设置的,我们会收到错误消息。不一致是由AvatarPropsdiv 的默认值在AvatarProps 中指定的方式引起的,而不是由该解决方案引起的。

以上是关于TypeScript 参数类型推断失败的主要内容,如果未能解决你的问题,请参考以下文章

从类型的默认参数推断 TypeScript 类型

在 TypeScript 中,如何在具有多个类型参数的方法中推断出泛型类型参数?

typescript 从子类调用的构造函数参数推断类型

如何使 TypeScript 从数组中推断参数的数量和类型

TypeScript:当提供变量 CLASS 作为函数参数时,将返回类型推断为该类的实例(仅来自参数)

TypeScript类型检查机制