TypeScript 参数类型推断失败

TypeScript parameter type inference failure

我为此创建了一个 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.

每当我看到函数类型依赖于参数值的要求时,第一个想到的就是"use overloads":

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

Update 我假设在下面的代码中 foof 是 React 组件,并且 foo 公开 f作为其 属性,以及 f 具有的所有属性。

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

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

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

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

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

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

问题

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

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

可能的解决方案

让我先声明一下,我不知道这是否适用于所有情况,或者这是否取决于某些会改变的编译器行为。我确实发现它是一个非常脆弱的解决方案,更改任何细节,即使是可以合理地认为 equivalent 的东西也是如此,它不会起作用。我能说的是,正如所展示的那样,它适用于我对其进行的测试。

解决方案是利用 属性 类型取自构造函数的 return 值这一事实,如果我们对默认泛型参数进行一些简化,我们得到我们想要的推论。唯一的问题是检测到我们正在处理默认的泛型参数。我们可以通过向默认参数添加一个额外字段然后使用条件类型对其进行测试来做到这一点。还有一个警告,这不适用于可以根据使用的属性推断 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>
  )

应用于materialui

我在 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 天:)