Reputation: 31579
I created a bug report for this, but perhaps someone has an idea for a workaround. Basically I want to have some arguments to a function based conditionally on the type of another:
declare function foo<P>(params: { f: (p: P) => void } & P): void;
foo({
f(params: { bar(x: number): void }) {},
bar(x) {},
});
So there is a special parameter f
which has a function type, and whatever properties that function expects must be included as arguments to foo
. Here again the parameter to bar
is inferred as any
but should be number
.
Upvotes: 10
Views: 885
Reputation: 249586
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 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>
)
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 : 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>>>
}
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 :)
Upvotes: 2
Reputation: 51619
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 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
});
Upvotes: 4