TypeScript parameter type inference failure TypeScript parameter type inference failure typescript typescript

TypeScript parameter type inference failure


Whenever I see a requirement to have function type that depends on argument values, "use overloads" is the first thing that comes to mind:

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 I assume that in the following code foo and f are React components, and foo exposes f as its property, as well as all the properties that f has.

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

There is a way to rewrite it, it you don't insist that the name of f (which is a property of foo), as well as types of f properties must be declared inline within foo arguments when calling it.

You can declare a type that describes properties of such composed component, it takes two generic parameters: the type of inner component properties, and the name of inner component in outer properties:

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


As we discussed in the comments the scenario is actually related to react components and the way the new generic component feature interacts with default generic parameters.

The problem

If we have a react component that has a generic parameter and the default value for the generic parameter has function field, the parameters to the function we pass in will not be inferred. A simple example would be:

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

Possible solution

Let me preface this with a caveat, I don't know it this works in all situations, or whether this depends on some compiler behavior that will change. What I did find is that it is a very fragile solution, change any detail, even with something that might reasonably be considered equivalent and it will not work. What I can say is that as presented it worked for what I tested it on.

The solution is to take advantage of the fact that the property types are taken from the return value of the constructor, and if we simplify things a little bit for the default generic parameter we get the inference we want. The only problem is detecting that we are dealing with the default generic parameter. We can do this by adding an extra field to the default parameter and then test for it using conditional types. Just one more caveat, this does not work for simple cases where P can be inferred based on used properties, so we will have an example where the component extracts properties from another component (which is actually closer to your use case):

// Do not change { __default?:true } to DefaultGenericParameter it only works with the former for some reasontype IfDefaultParameter<C, Default, NonDefault> = C extends { __default?:true } ? Default : NonDefaulttype 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 classclass _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>  )

Applied in material ui

The one sample I have tried in the material-ui repo is the Avatar component and it seems to work:

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 : NonDefaulttype 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>>>}

Usage

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>

Good luck with this, let me know if any of it helped. It was a fun little problem that has been keeping me awake for 10 days now :)