Using higher order component in Typescript - reactjs

I am trying to use react-i18next with Typescript, and I am running into a few issues.
Here's my interface for the props that I am expecting.
import * as i18n from 'i18next';
export interface ReactI18nProps {
t: i18n.TFunction;
}
I then have a navigation component, which I compose.
import { translate } from 'react-i18next';
function Navigation({ t }: ReactI18nProps) {
const userProfile = JSON.parse(localStorage.getItem('profile') || '');
return (
...
);
}
export default translate('navigation')(Navigation);
And now I simply want to render this component that I have composed
function Appliances({ location }: NavLinkProps) {
const userId = location && location.state.userId;
return (
<div>
<Navigation />
</div>
);
}
However, at this juncture, TS fails me and I get the following error
Type '{}' is not assignable to type 'IntrinsicAttributes & ReactI18nProps'.
Type '{}' is not assignable to type 'ReactI18nProps'.
Property 't' is missing in type '{}'.
Could someone please explain what I am doing incorrectly.

As the error message suggests:
Property 't' is missing in type '{}'
Your component expects the props to be:
interface ReactI18nProps {
t: i18n.TFunction;
}
But you're not passing this t:
<Navigation />
You can either make this t property optional:
interface ReactI18nProps {
t?: i18n.TFunction;
}
Or satisfy it:
<Navigation t={ (key: string, options: i18n.TOptions) => return getTranslation(key, options) } />
Edit
Based on the type definitions for react-i18next translation function:
function translate<TKey extends string = string>
(namespaces?: TKey[] | TKey, options?: TOptions)
: <C extends Function>(WrappedComponent: C) => C;
The type of the component you pass is the exact type you get back, meaning that the same props type is used.
In this case, you'll need to use an optional t because sometimes it is used without this property (when you call it) but then it is used with t (when the translate method calls it).
To avoid the compiler complaining about t possibly be undefined you can use the non-null assertion operator:
function Navigation({ t }: ReactI18nProps) {
...
const translation = t!(...);
...
}
Notice that it's using t! instead of just t.

Related

How to write a Typescript React Component Wrapper

A custom ReactJS wrapper component should be used to wrap code and only render it, if the user has the required permission. Therefore, it needs to accept two parameters, the children that shall be rendered as well as the permission it needs to check for.
The idea is to use it like this:
const AdminTools = () => {
return (
<RequirePermission require_permission="ui:see_admin_menu">
<>
<h1>Sudo mode</h1>
<Link href="/intern/users/">
<a>
<ActionCard link>User management</ActionCard>
</a>
</Link>
</>
</RequirePermission>
);
};
What I came up with so far is the following code:
const RequirePermission = (children: React.FC<{}>, required_permission: string) => {
const user = useContext(AuthContext);
let hasPermission = false;
if (user.state == AuthState.Authorized) {
user.user?.realm_access?.roles.forEach(role => {
if (permissions[role].includes(required_permission)) {
hasPermission = true;
}
});
}
if (hasPermission) {
return children;
} else {
return <div />;
}
};
export default RequirePermission;
When using the code snippet as described above, the following error is thrown:
Type '{ children: Element; require_permission: string; }' is not assignable to type 'IntrinsicAttributes & FC<{}>'.
Property 'children' does not exist on type 'IntrinsicAttributes & FC<{}>'.ts(2322)
'RequirePermission' cannot be used as a JSX component.
Its return type 'FC<{}> | Element' is not a valid JSX element.
Type 'FunctionComponent<{}>' is missing the following properties from type 'Element': type, props, keyts(2786)
I don't really understand the error message to be frank. Any help would be much appreciated.
//Edit:
Error messages given by proposed code:
This JSX tag's 'children' prop expects a single child of type 'ReactChildren', but multiple children were provided.ts(2746)
and
'RequirePermission' cannot be used as a JSX component.
Its return type 'Element | ReactChildren' is not a valid JSX element.
Type 'ReactChildren' is missing the following properties from type 'Element': type, props, key
Try this, importing ReactChildren from react.
I wish i could explain this better, but i know typescript is expecting a JSX/TSX element, so we could use ReactElement for the return type.
For a better explanation on the children type
https://stackoverflow.com/a/58123882/7174241
import React, { ReactChildren, ReactElement, ReactNode } from "react";
interface RequireType {
children: ReactChildren | ReactNode | ReactElement;
required_permission: string;
}
const RequirePermission = ({ children, required_permission }: RequireType):ReactElement => {
const user = useContext(AuthContext);
let hasPermission = false;
if (user.state == AuthState.Authorized) {
user.user?.realm_access?.roles.forEach((role) => {
if (permissions[role].includes(required_permission)) {
hasPermission = true;
}
});
}
return <>{hasPermission ? children : null}</>
};
export default RequirePermission;

Is there a way to extract the type of the props of a JSX Element?

My intent is to extract the props out of a given JSX element, is it possible?
This was pretty much my failed attempt...
Thanks in advance for any help ;)
function getComponentProps<T extends React.ReactElement>(element: T): ExtractProps<T>;
function Component({ name }: { name: string }) {
return <h1>{name}</h1>;
}
type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends React.ComponentType<infer TProps>
? TProps
: TComponentOrTProps;
const componentProps = getComponentProps(<Component name="jon" />); //type is JSX.Element
For the most part, you can't do this.
In theory, the React.ReactElement type is generic with a type parameter P that depends on the props. So if you were to have a strongly-typed element then you could work backwards.
type ElementProps<T extends React.ReactNode> = T extends React.ReactElement<infer P> ? P : never;
In reality, you will only get a correct props type if you create your element through React.createElement rather than JSX.
Any JSX element <Component name="John" /> just gets the type JSX.Element which obviously has no information about the props so you cannot work backwards from that to a props type.
const e1 = React.createElement(
Component,
{ name: 'John' }
)
type P1 = ElementProps<typeof e1> // type: {name: string}
console.log(getElementProps(e1)); // will log {name: "John"}
const e2 = <Component name="John" />
type P2 = ElementProps<typeof e2> // type: any
console.log(getElementProps(e2)); // will log {name: "John"}
Playground Link
It is much easier to approach the situation from a different angle. You will be able to derive the correct props type if your function takes a component like Component or div rather than a resolved element. You can use the ComponentProps utility type for function and class components and the JSX.IntrinsicElements map for built-in ones.
You can extract type of the Props for any component using
React.ComponentProps<typeof T>
You can refer this TS Playground for more options
import * as React from 'react';
type TProps = {
name:string;
age:number;
isStackoverflow:boolean;
}
const App = (props:TProps) => <div>Hello World</div>;
//You can extract any components props like this.
type AppProps = React.ComponentProps<typeof App>;
`
This is not possible for getComponentProps(<Component name="jon" />);, since written out JSX-Elements always result in the JSX.Element-type which doesn't give any additional type information which you could extract. It would be possible if you extract it from the component function itself:
export function Component({ name }: { name: string}) {
return <h1>{name}</h1>;
}
function getComponentProps<T extends (...args: any[]) => JSX.Element>(element: T): Parameters<T>[0] {
return null as any;
}
const test = getComponentProps(Component); // { name: string;}
This solution uses the utility type parameter, which infers all arguments from a function. We then index the first argument since the prop object is the first argument of a pure jsx function. Class components would need a different solution, though.

ReactElement type casting

This is more of a Typescript and React question but for full disclosure, I'll mention that I ended up here while trying to create a custom header using react-navigation.
I have a simple ImageButton (functional) component, which accepts the following props:
export interface ImageButtonProps extends TouchableOpacityProps {
transparent?: boolean;
}
My custom header looks (roughly) like this:
export default function Header(props: StackHeaderProps) {
const options = props.scene.descriptor.options;
const HeaderRight = options.headerRight as (() => ReactElement<ImageButtonProps>);
return (<HeaderRight transparent={true}/>); // <-- Typescript complains here
}
It's safe to assume that options.headerRight will always return an ImageButton, which in fact renders just fine. The only issue is that the property transparent is always undefined and also Typescript throws the following error:
TS2322: Type '{ transparent: boolean; }' is not assignable to type 'IntrinsicAttributes'.   Property 'transparent' does not exist on type 'IntrinsicAttributes'.
So my question is, how can I properly cast a ReactElement to my custom component, ImageButton and its props?
Your error is because the type you casted it to doesn't take any arguments or props.
This means, a function which takes no arguments, returns React.Element:
() => ReactElement<ImageButtonProps>
You want it to take the props, so you need to type in the props:
(props: ImageButtonProps) => ReactElement<any>
Or
React.ComponentType<ImageButtonProps>
Also, TypeScript doesn't support typechecking ReactElement, so there's no need for doing ReactElement<ImageButtonProps>.
Also see https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html

How to isolate known properties in an intersection of a generic type and a non-generic type

I have an HOC that takes a withPaper prop but does not pass it to the component it will render.
import React, { ComponentType, FC } from "react";
import { Paper } from "#material-ui/core";
interface WithOptionalPaperProps {
withPaper?: boolean;
}
export const withOptionalPaper = <Props extends object>() => (
Component: ComponentType<Props>
) => ({ withPaper, ...otherProps }: Props & WithOptionalPaperProps) => {
if (withPaper) {
return (
<Paper>
<Component {...otherProps as Props} />
</Paper>
);
}
return <Component {...otherProps as Props} />;
};
// Code below shows how the code above will be used.
interface NonPaperedComponentProps {
text: string;
className: string;
}
const NonPaperedComponent: FC<NonPaperedComponentProps> = props => {
return <h1 className={props.className}>{props.text}</h1>;
};
// Code will be used like an HOC.
// 'withPaper' prop can be optionally added to wrap the underlying component in 'Paper'
const OptionalPaperedComponent = withOptionalPaper<NonPaperedComponentProps>()(
NonPaperedComponent
);
// All props except 'withPaper' should be passed to 'NonPaperedComponent'
const renderedComponent = (
<OptionalPaperedComponent withPaper className="Hello" text="Hello There" />
);
I have removed the errors by type casting with otherProps as Props. Without them it produces the error 'Props' could be instantiated with a different subtype of constraint 'object'
https://codesandbox.io/s/gallant-shamir-z2098?file=/src/App.tsx:399-400
I would have assumed that since I have destructured and isolated the known properties from Props & WithOptionalPaperProps the types would look like this:
{
withPaper, // type 'WithOptionalPaperProps["withPaper"]'
...otherProps // type 'Props'
}
How do I make it that the Component the withOptionalPaper returns with a withPaper prop without passing it to its children but still passing all the other props?
This is a limitation in how de-structured rest objects are types. For a long type TS did not even allow de-structuring of generic type parameters. In version 3.2 the ability to use rest variables with generic type parameters was added (PR) but the rest variable is typed as Pick<T, Exclude<keyof T, "other" | "props">>, or equivalently Omit<T, "other" | "props">. The use of the conditional type Exclude will work fine for the consumers of this function if T is fully resolved (ie not a generic type parameter) but inside the function, typescript can't really reason about the type that contains the Exclude. This is just a limitation of how conditional types work. You are excluding from T, but since T is not known, ts will defer the evaluation of the conditional type. This means that T will not be assignable to Pick<T, Exclude<keyof T, "other" | "props">>
We can use a type assertion as you have, and this is what I have recommended in the past. Type assertions should be avoided, but they are there to help out when you (ie the developer) have more information than the compiler. This is one of those cases.
For a better workaround we could use a trick. While Omit<T, "props"> is not assignable to T it is assignable to itself. So we can type the component props as Props | Omit<Props, "withPaper">. Since Props and Omit<Props, "withPaper"> are essentially the same type, this will not matter much, but it will let the compiler assign the rest object to the component props.
export const withOptionalPaper = <Props extends object>(
Component: ComponentType<Props | Omit<Props & WithOptionalPaperProps, keyof WithOptionalPaperProps>>
) => ( {withPaper, ...otherProps }: Props & WithOptionalPaperProps) => {
if (withPaper) {
return (
<Paper>
<Component {...otherProps} />
</Paper>
);
}
return <Component {...otherProps } />;
};
Playground Link

TypeScript not inferring props from React.ComponentType

I have the following function which I want to use to take in a ComponentType and its props in order to allow me to inject those props along with the RouteComponentProps
const routeComponentFactory = <TProps extends {}>(
Component: React.ComponentType<TProps>,
props: TProps
) => {
return (routeProps: RouteComponentProps) => {
return <Component {...routeProps} {...props} />;
};
};
This function works correctly if I explicitly specify TProps, for example:
interface MyComponentProps { a: number; b: number; }
const MyComponent: React.FunctionComponent<MyComponentProps> = () => null;
routeComponentFactory<MyComponentProps>(MyComponent, {});
I get an error there for not providing a and b in the object.
However, if I remove the explicit <MyComponentProps>, the error goes away and I am allowed to call the function with an empty object.
How can I make TypeScript properly infer MyComponentProps?
If you enable strictFunctionTypes you will get an error:
Type 'FunctionComponent<MyComponentProps>' is not assignable to type 'FunctionComponent<{}>'.
There are two possible inference sites for TProps the arguments Component and props both contain the type parameter TProps so typescript tries to find a type that will make both sites happy. Since if TProps were {} both the argument {} and the props type MyComponentProps would be assignable to it typescript infers TProps to be {} in order to keep everyone happy.
The reason that under strictFunctionTypes you do get an error is that by default function type behave bivariantly (so a function (p: MyComponentProps) => JSX.Element is assignable to (p: {}) => JSX.Element). Under strictFunctionTypes function types behave contravariantly so such an assignment is disallowed.
The solution to get an error even without strictFunctionTypes is to decrease the priority of the props inference site, so the compiler picks what is good for Component and checks it against props. This can be done using an intersection with {}:
const routeComponentFactory = <TProps extends {}>(
Component: React.ComponentType<TProps>,
props: TProps & {}
) => {
return (routeProps: any) => {
return <Component {...routeProps} {...props} />;
};
};
interface MyComponentProps { a: number; b: number; }
const MyComponent: React.FunctionComponent<MyComponentProps> = () => null;
routeComponentFactory(MyComponent, {}); // Argument of type '{}' is not assignable to parameter of type 'MyComponentProps'
I cannot see it allowing me to call the function with an empty object but in my case it is complaining that the MyComponent prop is invalid.
It seems the problem is it is inferring the type from the second parameter - I guess because it is matching on the simplest type.
You could define your function like this to force it to infer the correct type:
const routeComponentFactory = <TProps extends {}>(
Component: React.ComponentType<TProps>
) => (
props: TProps
) => {
return (routeProps: RouteComponentProps) => {
return <Component {...routeProps} {...props} />;
};
};
And then call it like this:
routeComponentFactory(MyComponent)({});
And it should complain about the empty object correctly in this case.

Resources