Typescript React component type inference

145 views Asked by At

I am having some issue finding a way to achieve what I need to do with TS in a React monorepo. The structure is kind of classic:

...repo related stuff
packages /
    package-a
    package-b
    package-c
   ...other packages

where:

  • package-a is a collection of base react component which are used throughout the whole repo. It has no dependencies over the other two packages (of course because of circular dependency).

  • package-b has a different sets of components which are linked to some specific external libraries. It has a dependency over package-a.

  • package-c uses components from both package-a and package-b. Depends on both a and b packages.

What I am trying to achieve is:

  • in package-a I have a component with a linkComponent prop. This prop is another component which has some specific props. The thing is this component can have a type defined in package-a or in package-b depending to the component it is passed as prop.

This is the component (with less props obviously):

import {FC} from "react";
import type {IBaseLinkProps} from 'package-a';
import {BaseLink} from 'package-a';

interface IBreadcrumbsProps<P extends IBaseLinkProps> {
    linkComponent?: FC<P>;
}

function Breadcrumbs<P extends IBaseLinkProps>({
     linkComponent: LinkComponent = BaseLink
}: IBreadcrumbsProps<P>) {
    return (
        <LinkComponent to={'/path1'}>{'path1'}</LinkComponent>
    );
}

export type {IBreadcrumbsProps};
export {Breadcrumbs};

This is the BaseLink in package-a:

import type {FC, ReactNode} from "react";

interface IBaseLinkProps {
    to: string;
    children: ReactNode;
}

const BaseLink: FC<IBaseLinkProps> = ({children, to}) => (
    <a href={to}>{children}</a>
);

export type {IBaseLinkProps};
export {BaseLink};

and this is the link in package-b:

import type {FC} from "react";
import type {IBaseLinkProps} from 'package-a'

interface IExtendedLinkProps extends Omit<IBaseLinkProps, 'to'> {
    to: {
        pathname: string;
    };
}

const ExtendedLink: FC<IExtendedLinkProps> = ({children, to}) => (
    <a href={to.pathname}>{children}</a>
);

export type {IExtendedLinkProps};
export {ExtendedLink};

Now the thing is I need to use this component with both IBaseLinkProps and IExtendedLinkProps without the possibility to define it in the Breadcrumbs component in package-a because of circular dependency with package-b.

Is there a way to achieve this without getting the error:

TS2322: Type  FC<IBaseLinkProps>  is not assignable to type  FC<T> 
Types of property  propTypes  are incompatible.
Type  WeakValidationMap<IBaseLinkProps> | undefined  is not assignable to type  WeakValidationMap<T> | undefined 
Type  WeakValidationMap<IBaseLinkProps>  is not assignable to type  WeakValidationMap<T> 

or:

TS2322: Type  { children: ReactNode; to: string; }  is not assignable to type  T 
 { children: ReactNode; to: string; }  is assignable to the constraint of type  T , but  T  could be instantiated with a different subtype of constraint  IBaseLinkProps 

The closest working solution I found is something like:

<Breadcrumbs<IExtendedLinkProps> linkComponent={Link}/>

where the linkComponent in the props is defined as: linkComponent?: FC<T>; and destructured in the props as: linkElement: LinkElement = Link.

But it gives:

TS2344: Type  IExtendedLinkProps  does not satisfy the constraint  IBaseLinkProps 
Types of property  to  are incompatible.
Type  {to: {pathname: string}}  is not assignable to type  string 

Update#1

My ExtendedLink component must extend the BaseLink and the RouterLinkProps which comes from react-router:

interface INavLinkProps
    extends Omit<IBaseLinkProps, 'to'>,
        Omit<RouterLinkProps, 'to'> {
    children: ReactNode;
    to: IDestToken;
}

const Link: FC<INavLinkProps> = ({children, to}) => {
    const {getPathTo} = useNavigation();

    const path = useMemo(() => getPathTo(to), [getPathTo, to]);

    if (!path) {
        throw new Error(`Could not find path for destination: ${to.KEY}`);
    }

    return (
        <RouterLink to={path}>
            {children}
        </RouterLink>
    )
}

so it cannot have the to to be a string because it would mean to add additional type checking also in other places. Furthermore if I try I receive (consider that IDestToken is an object similar to the one I originally posted): TSError

1

There are 1 answers

1
shantr On

Your issue here is that IExtendedLinkProps.to ({ pathname: string }) is not compatible with IBaseLinkProps.to (string)

Which is why you cannot use Breadcrumbs with ExtendedLink

Your breadcrumbs render method:

        <LinkComponent to={'/path1'}>{'path1'}</LinkComponent>

There would obviously be an issue if you used ExtendedLink as it expect an object and not a string

One way to make it compatible would be to make ExtendedLink accept both string and object

interface IExtendedLinkProps extends Omit<IBaseLinkProps, 'to'> {
    to: string | {
        pathname: string;
    };
}

const ExtendedLink: FC<IExtendedLinkProps> = ({children, to}) => (
    <a href={typeof to === 'string' ? to : to.pathname}>{children}</a>
);