extend inline typescript interface on function - reactjs

I have a pretty simple React component which takes a required prop, active.
Setting the inline interface for this one prop is quite simple. But now, I need to account for the other types of props that can be passed onto the component.
Luckily, Native supplies the props for this element, so I don't have to recreate the interface for it. But I'm struggling to figure out how to extend my current inline interface to get it to work.
I've tried this approach:
const BarTabName = ({ active, ...props }: { active: boolean; } extends TextProps) => {
return <Text {...props} />;
};
But this just results in the error:
'?' expected.ts(1005)
If possible, I'm trying to avoid creating a separate interface outside of the parameters.

Use an intersection:
const BarTabName = ({ active, ...props }: { active: boolean; } & TextProps) => {

Related

Hide generic type prop on export

I have a small set of components in which a Wrapper is going to manipulate its children (therefore referred to as Components by injecting a prop into each of the children via cloneElement.
The gotcha here is that Component props are of a generic type. When I expose Component on the code, I don't want one of its props to be on the signature, because it will be automatically injected by the Wrapper component.
I have a concise example which shows what I mean:
types.ts
export type SomeObject = {
someKey: string;
};
type PropThatWillBeInjected<T extends SomeObject> = {
fn: (value: string) => T;
};
export type WannaBePropTypes = {
name: string;
};
export type PropTypes<T extends SomeObject> = PropThatWillBeInjected<T> &
WannaBePropTypes;
Important: PropTypes<T> is what Component expects, but as a programmer, I want WannaBePropTypes to be the signature of this component.
Moving on...
Component.tsx
function Component<T extends SomeObject>(props: PropTypes<T>) {
const { fn, name } = props;
const result = fn(name);
return <div>Hello, {result.someKey}</div>;
}
export default Component;
Wrapper.tsx
function Wrapper(props: { children: ReactNode }) {
const { children } = props;
return (
<div id="wrapper">
{React.Children.map(
children as ReactElement<PropTypes<SomeObject>>,
(child, index) =>
cloneElement(child, {
...child.props,
fn: (value: string) => ({
someKey: `${value}-${index}`,
}),
})
)}
</div>
);
}
export default Wrapper;
As expected, when I try to use these components as the following, the code works but the compiler complains:
<Wrapper>
<Component name="Alice" />
<Component name="Bob" />
</Wrapper>
Property 'fn' is missing in type '{ name: string; }' but required in type 'PropThatWillBeInjected'.(2741)
Is there a way to cast Component so I don't need to pass fn manually? I know there's a way when the prop types is not generic...
What I've tried:
Making fn optional: works, but this is not the solution I'm looking for;
Wrapping Component with another component and passing a noop to Component: works, but I don't want to create this unnecessary wrapper;
A playground with this sample code: StackBlitz
If I inderstand your problem correctly, you want to call Component as <Component name="Alice" /> and there should be some internal logic for two cases: when fn was passed and when not. If so, you can create unnecessary type (instead of unnecessary wrapper) which will be one of WannaBePropTypes or full props. This is like some combination of your try#1 and try#2:
type FullProps<T extends SomeObject> = PropThatWillBeInjected<T> & WannaBePropTypes;
type PropTypes<T extends SomeObject> = FullProps<T> | WannaBePropTypes;
So fn is optional until you define children as ReactElement<FullProps<SomeObject>> in Wrapper component. This is how to tackle with Typescript only.
BTW: maybe you can just pass array of WannaBePropTypes objects into Wrapper instead of children? This sounds better if <Component name="Alice" /> should do nothing by itself.

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.FunctionComponent with generics in typescript

I'm creating a component that takes an arbitrary list of values and a transformer that will render a specific value. The values can be of any type, as the rendering is handler by the transformer, but the compiler is complaining about the generics parameter.
Here is a minimal example:
interface MyListParams<T> {
values: T[];
transformer: (value: T) => JSX.Element
}
export const MyList: React.FunctionComponent<MyListParam<T>> = ({
}) => <>
{values.map(transformer)}
</>
This code gives me the error Cannot find name 'T'. in code React.FunctionComponent<MyListParam<T>>.
I do realize that I have to tell typescript that this function is using generics, but I fail to find the correct place to do so.
I don't think there's a way to pass a type to the component when using React.FC, so you can try:
interface MyListParams<T> {
values: T[];
transformer: (value: T) => JSX.Element;
}
const MyList = <T extends object>(props: PropsWithChildren<MyListParams<T>>) => <></>;
The generics is on the interface - meaning you want to reuse the interface.
In your example you are using the interface so you need to specify its type:
// T is string
export const MyList: React.FunctionComponent<MyListParams<string>> = ({
values, transformer
}) => <>{values.map(transformer)}</>
If the generics on the function see official examples (not a real use case in function component).

Strictly typing React higher order component which consumes properties with TypeScript

I'm trying to type some higher order React components which are more complex than only injecting properties. These HOCs must be able to:
receive properties that are not (or only optionally) forwarded to the wrapped component
provide some new properties to the wrapped component
take additional arguments when they are created
From what I can tell, this means we must have three types:
The properties the HOC provides to the wrapped component
The properties the HOC uses, but which the wrapped component not necessarily cares about
The properties of the wrapped component that the HOC doesn't care about
I have the following code that works, but I can't find a way to avoid the any cast when passing the props on to the wrapped component:
import React, { ComponentType } from 'react';
// Props of the wrapped component without the props provided by the HOC
type StrictWrappedProps<WrappedProps, InjectedProps> = Omit<
WrappedProps,
keyof InjectedProps
>;
type WrappedComponent<WrappedProps, InjectedProps> = ComponentType<
WrappedProps & InjectedProps
>;
type HOCType<WrappedProps, InjectedProps, OwnProps> = ComponentType<
OwnProps & StrictWrappedProps<WrappedProps, InjectedProps>
>;
type LengthifyOwnProps = {
name: string;
};
type LengthifyInjectedProps = {
length: number;
};
export const lengthify = <WrappedProps extends {}>(
Wrapped: WrappedComponent<WrappedProps, LengthifyInjectedProps>,
multiplier: number
): HOCType<WrappedProps, LengthifyInjectedProps, LengthifyOwnProps> => ({
name,
...props
}) => {
const length = name.length * multiplier;
return <Wrapped {...props as any} length={length} />;
};
Here is an example usage of that HOC:
const myComp = ({ bool, length }: { bool: boolean; length: number }) => (
<p>
Bool is: {bool}. Length is: {length}
</p>
);
const EnhancedMyComp = lengthify(myComp, 2);
const usage = () => <EnhancedMyComp bool={true} name="Hello" />;
The any cast is a small workaround, but I would like to find the strict way of typing this.
The React TypeScript cheat sheet on HOCs links to this bug in the TypeScript repo, but it seems to be more or less solved, and only relates to HOCs that inject properties.
Other questions here on StackOverflow also seem to to be about injecting properties, or confusion about the ComponentType type.

A proper way to reuse existing type definitions

When using a react component that passes some props to another react child component, I find myself rewriting some type definitions to the parent that was already defined in the child component.
interface ParentProps {
onChange: (value: string) => void; // I end up rewriting this, when it was already written on ChildProps interface.
}
const Parent: React.FC<ParentProps> = ({ onChange }) => {
return <Child onChange={onChange} label="Label 1" />;
};
// Child component. Could be imported from a third party library.
interface ChildProps {
onChange: (value: string) => void;
label: string;
}
const Child: React.FC<ChildProps> = ({ onChange }) => {
return <MyComponent onChange={onChange} />;
};
Are there any techniques to avoid rewriting type definitions?
Depends how much of ChildProps you want to reuse.
If you want to reuse just a couple of properties, you can use in indexed type query to get the type of a specific property:
interface ParentProps {
onChange: ChildProps['onChange']
}
Or you can define ParentProps to extend ChildProps if you want to reuse all properties:
interface ParentProps extends ChildProps {
}
Or you can pick just a subset using Pick:
interface ParentProps extends Pick<ChildProps, 'onChange'> { // Pick<ChildProps, 'onChange' | 'label'> to pick more
}
Or if you want to pick all except a subset you can use Omit
interface ParentProps extends Omit<ChildProps, 'label'> { // Omit<ChildProps, 'onChange' | 'label'> to omit more
}

Resources