#Polymorphic React HOC, help with types

18 messages · Page 1 of 1 (latest)

trim osprey
#

Hello,

I'm trying to create a getLink React HOC that returns:

  1. an "a" intrinsic element if isExternal prop is true
  2. a component taken from its argument (polymorphic) if isExternal is false

A basic JS implementation would be like this:

export const getLink = (LinkImpl) => {
  const Link = forwardRef((props, ref) => {
    const { isExternal, noFollow, ...rest } = props;

    const wrapperProps = {
      rel: noFollow ? "nofollow" : undefined,
      target: isExternal ? "_blank" : undefined,
      ...rest,
    };

    if (isExternal) {
      return <a {...wrapperProps} ref={ref} />;
    }

    return <LinkImpl {...wrapperProps} ref={ref} />;
  });
  Link.displayName = "Link";

  return Link;
};

I'm struggling to find a way to correctly type this in TS.
What I've come up with so far:
https://tsplay.dev/w8Dvdw

The types seems to be working somewhat but there are some issues:

  1. Type 'UrlObject' is not assignable to type 'string'. on the return statement (line 41).
    Is it safe to assume that this error can be ignored as the href prop is inherited from the LinkImpl component and thus they should match in any case?
  2. I manually casted two variables with an as (line 27 and 32) statement although props is of type LinkProps and should contain all the referenced properties in any case.

What is the best way to refactor the code to avoid those issues?

The returned component props type should:

  • Expose all implementation component props
  • Add noFollow and isExternal boolean props
  • Force href to be string when isExternal is true
trim osprey
#

After taking a look at this and this issues I came up with this new solution:
https://tsplay.dev/mql8rN

Both previous issues seem to be resolved, but now I'm stuck with this 😦 (line 40):

'{ rel: string | undefined; target: string | undefined; } & Omit<PropsWithoutRef<LinkProps<P>>, "href" | "noFollow" | "isExternal"> & { ...; }' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'LinkImplRequiredProps'.(2322)

Anyone willing to take a look? 🙏

GitHub

Search Terms Mapped Type Union Suggestion This might be a bug or an intentional consequence of the original design, but mapped types do not distribute over union types. The example below demonstrat...

GitHub

TypeScript Version: 3.2.0-dev.20181110 Search Terms: Pick preserve optional union Code type A = { optional?: string; other: string; } type B = { optional?: string; other: string; } type SimplePick ...

trim osprey
#

!helper

deft thornBOT
#
silver377#0

Preview:```ts
import type {UrlObject} from "node:url"
import React, {forwardRef} from "react"
import type {
ComponentPropsWithoutRef,
ComponentType,
} from "react"
import NextLink from "next/link"

type LinkImplRequiredProps = {
href?: string | UrlObject
}
...```

trim osprey
fickle steppe
#

@trim osprey TBH, I'd probably look for a non-generic HOC approach - maybe a standard contract for what props a link implementation can accept, maybe split the external case out and handle that separately

trim osprey
#

@fickle steppe Unfortunately I don't think I can do it without generics, this is a shared component and each project in a monorepo is using a specific link implementation (href has a different type in each project).
Even splitting the external case out I still need generics to cascade the link implementation props, am I right? 🤔

fickle steppe
#

Well I think if you pulled out the external case, you'd just be left with a small amount of logic that calculates rel and target and maybe you'd just get rid of this HOC and move that logic somewhere else.

#

Unless there's a lot of logic and this is a simplified example which omits

#

One approach I've used for links in the past is to have a routing utility that generates onClick and href properties (and IIRC a few others) and I spread them into the things that use them. If I get a chance I can pull up a more specific example

trim osprey
#

It would be awesome if you can provide an example!

trim osprey
#

(I cannot paste the direct link to TS Playground as it exceeds the character limit unless I pay a subscription to Discord Nitro 😅)

deft thornBOT
#
retsam19#0

Preview:```ts
import {} from "react";

const routeInfo = {
foo: "/foo",
bar: "/bar",
};

// Real navigation is some sort of SPA-friendly mechanism not just changing the href
const buildNavigateFunc = (url: string) => () => (window.location.href = url);

const buildRoute = (url: string) => ({
...```

fickle steppe
#

Here's a simplified example of the sort of approach we've used

#

This basically lets you embed whatever routing logic you want in the onClick handler and as long as the link component accepts an onClick it's probably fine.

#

My 'routeInfo' is just the url in that example, but you can make it anything you want like: { url: "/foo", isExternal: false } and adjust the logic inside buildRoute accordingly.