Flow: optional prop that uses generic types - reactjs

I've got a react component that has a function prop called showModal. I've annotated this function elsewhere using generic types, but here, I want it to be an optional prop. Usually I do that with func?: () => void, but that syntax doesn't work in this case (I get Parsing error no matter where I place the ?):
type props = {
showModal<T>(T => React$Node, T): void,
}
How do I specify that showModal is optional? I've looked through the docs for Flow, but can't find anything relevant to this issue.

You need to tweak the function expression. Flow Try link
type Props = {
showModal?: <T>(T => React$Node, T) => void,
}
const foo: Props = {
showModal: (a, b) => undefined
}
const bar: Props = {
showModal: undefined
}

Related

Typescript React: generic-based callback event handler function as prop to component

I'm having trouble creating a generic-based callback event handler function that I want to pass as prop down to my component.
My goal: Allow user to pass a custom callback function that:
always takes in the same argument
an event (not a react/dom event handler event, its coming from a library)
can return different return type
for my use cases, this component is used in different context's so in one place a user will return a certain value over another
What I have Attempted
I have my onNodeClick function defined using generics & when i use it in isolation, it works.
// ✅ Simple example of calling a generic function
const onNodeClick = <T,> (event:any) => {
return null as unknown as T;
}
const string_result = onNodeClick<string>(event_from_somewhere)
However, when I try to pass this method as a prop to my component, I am getting errors. I'm unsure of how to resolve it
Live Typescript Code Playground
import React from 'react';
type NodeComponentProps = {
onNodeClick: <T>(event: any) => T;
};
export const NodeComponent = ({onNodeClick}: NodeComponentProps) => {
return null;
}
const Homepage = () => {
const handleNodeClick = <T,>(event: any): T => {
return null as unknown as T;
};
return (
<NodeComponent
onNodeClick={(event): string => {
const string_result = handleNodeClick<string>(event); // ✅ correct type
return string_result; // ❌ onNodeClick is throwing type error; see error message a few lines below
}}
/>
)
}
/*
Type '<T>(event: any) => string' is not assignable to type '<T>(event: any) => T'.
Type 'string' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'string
*/
I think you want the type to be generic, not the function. If the function is generic, then it should be callable with angle brackets and work correctly. For example, the identity function works with any type, so it should pass your generic function definition:
const Homepage = () => {
const onNodeClick = <T>(x: T): T => {
return x;
};
return (
<NodeComponent
onNodeClick={onNodeClick}
/>
)
}
However, the function you used will always return a string. If you tried to call it with <number>, it would fail. Therefore, not a generic function.
By contrast, if you make the type generic, you should be able to specify you are using it with a string:
Here is a Playground link
type NodeComponentProps<T> = {
onNodeClick: (event: any) => T;
};
export const NodeComponent = <T,>({onNodeClick}: NodeComponentProps<T>) => {
return null;
}

Infer function prop input type typescript

I have a component (called Container) which has a renderHeader prop. The type of this prop is
(input: {
scrollY: Animated.Value
onLayout: (e: LayoutChangeEvent) => void
headerHeight: number
}) => JSX.Element
I have two ways to pass the prop. Either pass an inline arrow function or declare it somewhere and pass its name as prop.
In the second one Typescript does not automatically infer the function's argument types. As there are several other props, using inline arrow functions make the code messy and less readable.
How can I use the approach below and make TS infer the function's argument types?
const renderHeader = ({ /* This argument's type should be inferred without explicitly defining it */ }) => {}
<Container renderHeader={renderHeader} />
TypeScript can't infer that for you, you'll have to tell it something.
One way that may be less clunky that providing the event type after the destructuring is to assign a type to the renderHeader constant. Provide yourself a utility type:
type LayoutChangeHandler = (e: LayoutChangeEvent) => void;
and then use it when creating the function:
const renderHeader: LayoutChangeHandler = ({/*...*/}) => {
// ...
};
TypeScript will infer the argument type from the function type.
Playground link

React with Typescript

I'm using typescript with React. Whenever I pass props to function or destructure the props to {text} e.g. I get an error that says "Binding element 'text' implicitly has an 'any' type". Although I can bypass this with using const Example = ({text}:any) => { return () } or const Example = ({text}:{text:string}) => { return () }
it is not efficient for me. I wanted to ask if you can teach me any good way to overcome this without props argument itself but the destructured version of it.
Note: I see that I couldn't explain my problem well. I'm using Typescript because I want to use its type checking functionality. Giving every prop "any" type is ok but when I try to give every prop its own type e.g const Example = ({Text, Number, RandomCrap,}: {Text: string; Number: number; RandomCrap: any;}) => {return ();} It looks like this. Is this the only way?
Answer: Instead of cramming my code like above I can define types elsewhere like
type PropsType = {Text: string; Number: number; RandomCrap: any;};
and use it in my function like const Header = ({ Text, Number, RandomCrap }: PropsType) => {return ()}
Thanks everyone :)
You can define a type for your whole props object:
type PropsType = {
text: string
}
const Example = ({text}: PropsType) => { return () }
// or
const Example1: React.FunctionComponent<PropsType> = ({text}) => { return () }

Wrapper function for JSX elements, typed via generics

Currently this function works fine
function wrapElement(elem: JSX.Element) {
return ({ ...props }) => React.cloneElement(elem, { ...props })
}
I use it like this, because this way I can get intelliSense for tailwind classes
const Btn = wrapElement(<button className="[A LOT OF TAILWIND UTILITY CLASSES]" />)
But I'm trying to get it to return same type as it receives, so I could get intelliSense for attributes on intrinsic HTML elements.
Right now inferred type is
function wrapElement(elem: JSX.Element): ({ ...props }: {
[x: string]: any;
}) => React.FunctionComponentElement<any>.FunctionComponentElement<any>
I tried some stuff and it all failed with all kinds of errors, at this point it feels like this could be to hacky, but maybe I don't understand something?
It is basically impossible to get the correct props from a JSX.Element. You can achieve the design that you want, but you should pass in the element name and the props as separate arguments rather than passing in a JSX.Element.
This code can accept an element name like 'button' or any React component. It returns a function component with the same props. I am not dropping any props from the returned component because it seems like you are using this for setting defaults rather than dropping requirements.
import React, { ComponentType, ComponentProps } from "react";
const wrapElement = <
C extends keyof JSX.IntrinsicElements | ComponentType<any>
>(
Component: C,
presetProps: Partial<ComponentProps<C>>
) => (props: ComponentProps<C>) => {
const merged: ComponentProps<C> = { ...presetProps, ...props };
return <Component {...merged} />;
};
const Btn = wrapElement("button", {
className: "[A LOT OF TAILWIND UTILITY CLASSES]"
});
const Dbl = wrapElement(Btn, { onClick: () => alert("clicked") });
const Test = () => {
return <Dbl>Click</Dbl>;
};
Typescript Playground Link
You might want to customize your merge behavior to combine className or style properties rather than overriding them.
Note: when I tried to merge the props inline like <Component {...presetProps} {...props} /> I got a weird error "Type Partial<ComponentProps<C>> & ComponentProps<C> is not assignable to type IntrinsicAttributes & LibraryManagedAttributes<C, any>." So that is why I am merging the props on a separate line and annotating the type as ComponentProps<C> instead of the inferred type Partial<ComponentProps<C>> & ComponentProps<C>.

React: Inject props through HOC without declaring types for them

I'm trying to do something kinda connect() in react-redux bindings.
Here's my HOC that injects props in a component:
export function withAdditionalProps<T, I>(
injectedProps: I,
WrappedComponent: React.ComponentType<T>,
): React.ComponentType<Omit<T, keyof I>> { ... }
It works okay if I declare injected props types (I generic type), but what If I want to do a HOC without declaring these types (omit injected props keys on-fly). How can I determine keys from the passed props?
I tried, for example, something like this:
export function withAdditionalProps<T>(
injectedProps: { [key: string]: unknown },
WrappedComponent: React.ComponentType<T>,
): React.ComponentType<Omit<T, keyof typeof injectedProps>> { ... }
const InjectedComponent = withAdditionalProps<AppState>(
{
counter: 0
},
(props) => (<div>{props.counter}</div>)
);
But it's not working correct: compiler throws an error when rendering the component. Look at the screenshot (testProp is a "native" prop of a component)
Maybe anyone can help me.
In short, your second example isn't possible yet in Typescript.
The issue is that the { [key:string]: unknown } type always captures all possible strings as keys, rather than narrowing to the concrete ones you use in a particular call, which would make usage of Omit here possible.
As it is, Omit used with { [key:string]: unknown } simply omits all possible keys and therefore all your native props in T. This may be possible in future via the negated types feature, but for now the only way is via declared type variables as in your first example.
However, those declared type variables in the function definition don't oblige you to also declare them for each call. See the code below - the compiler will infer both T and I from the concrete arguments you pass when you call the function. So in practice the only difference here is in the type definitions for your HOC, and honestly it seems like similar effort/readability either way.
function foo<T, I>(t: T, i: I): Omit<T, keyof I> {
return { ...t, ...i }
}
// notice explicitly declaring foo<{ a: number, b: number}, { c: number }>
// is NOT necessary. The correct types are just inferred from the args you pass.
const bar = foo({ a: 1, b: 2 }, { b: 3 })
bar.a // OK
bar.b // ERROR, since b is omitted
If you capture both the component's Props type and the injected props type in generic parameters, you can make this work:
export function withAdditionalProps<C extends unknown, I extends object>(
injectedProps: I,
WrappedComponent: React.ComponentType<C>,
): React.ComponentType<Omit<C, keyof I>> {
// ...
}
Rather than passing AppState as a generic parameter, make sure TypeScript has enough context to infer it. You can do this by declaring AppState in the wrapped component:
interface AppState {
counter: number;
}
const InjectedComponent = withAdditionalProps(
{
counter: 0,
},
(props: AppState) => <div>{props.counter}</div>,
);
const el = <InjectedComponent />; // OK
const InjectedComponent2 = withAdditionalProps(
{
counter2: 0,
},
(props: AppState) => <div>{props.counter}</div>,
);
const el2 = <InjectedComponent2 />; // error: Property 'counter' is missing
TypeScript won't let you specify some generic parameters but leave others inferred. If you want to pass AppState via a generic, the standard workaround is to use two functions, one with the explicit generic parameters and one with the inferred ones. Here's how that looks in your example:
export function withAdditionalProps<PropsType>(): <I extends unknown>(
injectedProps: I,
WrappedComponent: React.ComponentType<PropsType>,
) => React.ComponentType<Omit<PropsType, keyof I>> {
// ...
}
And here's how you use it:
interface AppState {
counter: number;
}
const InjectedComponent = withAdditionalProps<AppState>()({
counter: 0,
},
props => <div>{props.counter}</div>,
);
const el = <InjectedComponent />; // OK
const InjectedComponent2 = withAdditionalProps<AppState>()(
{
counter2: 0,
},
props => <div>{props.counter}</div>,
);
const el2 = <InjectedComponent2 />; // error: Property 'counter' is missing
I can't quite make out what's going on behind the error message in your screenshot. If you can share the text or a link, I can take a look.

Resources