TypeScript React + Generics Part I — Time-Saving Patterns
Tags: typescript, react, oop, nextjs
Published: 2023-06-13 08:17:57

## Time is Money

Using TSX + Generics strategically in any given project helps to establish clean coding patterns and ultimately save time in the long run. I wrote the following definitions earlier this afternoon after becoming frustrated with unorganized typedefs littering a plethora of files across a project codebase. The defs in question happened to be mostly imports from React of the Attribute-Element variety. That said, let's begin

```ts
import type React from "react";

export type RemoveFields<T, P extends keyof T = keyof T> = {
  [S in keyof T as Exclude<S, P>]: T[S];
};

export type ExtractPropsTargeted<T> = T extends 
  React.DetailedHTMLProps<infer U, Element>
  ? U
  : T;

export type PropsTargeted<
  T extends keyof globalThis.JSX.IntrinsicElements = 
  keyof globalThis.JSX.IntrinsicElements
> = {
  [P in T]: ExtractPropsTargeted<
    globalThis.JSX.IntrinsicElements[P]
  >;
}[T];

export type PropsExcludeTargeted<
  T extends keyof globalThis.JSX.IntrinsicElements = 
  keyof globalThis.JSX.IntrinsicElements,
  J extends keyof PropsTargeted<T> = 
  keyof PropsTargeted<T>
> = RemoveFields<PropsTargeted<T>, J>;

export type PropsIncludeTargeted<
  T extends keyof globalThis.JSX.IntrinsicElements = 
  keyof globalThis.JSX.IntrinsicElements,
  J extends keyof PropsTargeted<T> = 
  keyof PropsTargeted<T>
> = RemoveFields<
      PropsTargeted<T>, 
      Exclude<keyof PropsTargeted<T>, J>
    >;

```

### The `RemoveFields<T, keyof T>` type

Why bother writing our own `RemoveFields` helper when we can use the builtin `Omit` type? Because it is a stricter implementation of Omit; it also provides heightened intellisense

Let's take a look at the definitions for `Omit` and `RemoveFields` side-by-side:

```ts
type Omit<T, K extends string | number | symbol> = 
{ [P in Exclude<keyof T, K>]: T[P]; }

type RemoveFields<T, P extends keyof T = keyof T> = 
{ [S in keyof T as Exclude<S, P>]: T[S]; }

```

While the difference may seem trivial at first glance, a real-world example involving the mapping of props in the Nextjs `Link` Component should provide a clearer distinction

The Link Component in `next/link/client/link.d.ts` is defined as

```ts
declare const Link: React.ForwardRefExoticComponent<Omit<
  React.AnchorHTMLAttributes<HTMLAnchorElement>, 
  keyof InternalLinkProps
> & InternalLinkProps & {
    children?: React.ReactNode;
} & React.RefAttributes<HTMLAnchorElement>>;
```

However, we're only interested in the definitions within the external `React.ForwardRefExoticComponent<P>` wrapper, so we can simplify the typedef as follows 

```ts
type TargetedLinkProps = Omit<
  React.AnchorHTMLAttributes<HTMLAnchorElement>, 
  keyof InternalLinkProps
> & InternalLinkProps & {
    children?: React.ReactNode;
} & React.RefAttributes<HTMLAnchorElement>
```

With `Omit` in place in the `TargetedLinkProps` type and with the `strict` and `always-strict` flags turned on in our `tsconfig.json` file, there are no type errors in the above definition. However, if we swap `Omit` out in favor of `RemoveFields` we see errors appear immediately.

```ts
// replacing `Omit` with `RemoveFields` results in errors

type TargetedLinkProps = RemoveFields<
  React.AnchorHTMLAttributes<HTMLAnchorElement>, 
  keyof InternalLinkProps
> & InternalLinkProps & {
    children?: React.ReactNode;
} & React.RefAttributes<HTMLAnchorElement>
```

Why? `Omit` is forgiving or lenient when it comes to key-discrimination and allows for extraneous keys to be present that do not exist in a given type definition which is a double-edged sword. If precision is your aim, `RemoveFields` can simply be thought of as a souped-up ever-vigilant version of `Omit`.

So what about these errors? Not to worry, these errors can be remedied by dissecting the definition for `InternalLinkProps` and refactoring appropriately

#### `InternalLinkProps` = `LinkProps`

Nextjs exports a `LinkProps` type from `next/link` which has a 1:1 relationship with the `InternalLinkProps` type. The definition of `LinkProps` is as follows

```ts
type InternalLinkProps = {
    href: Url;
    as?: Url;
    replace?: boolean;
    scroll?: boolean;
    shallow?: boolean;
    passHref?: boolean;
    prefetch?: boolean;
    locale?: string | false;
    legacyBehavior?: boolean;
    onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
    onTouchStart?: React.TouchEventHandler<HTMLAnchorElement>;
    onClick?: React.MouseEventHandler<HTMLAnchorElement>;
};
```

For absolute clarity, I've included the type definitions for `Url`, `UrlObject`, and `ParsedUrlQueryInput` below

```ts
interface ParsedUrlQueryInput extends NodeJS.Dict<
    | string
    | number
    | boolean
    | ReadonlyArray<string>
    | ReadonlyArray<number>
    | ReadonlyArray<boolean>
    | null
>{}

interface UrlObject {
    auth?: string | null | undefined;
    hash?: string | null | undefined;
    host?: string | null | undefined;
    hostname?: string | null | undefined;
    href?: string | null | undefined;
    pathname?: string | null | undefined;
    protocol?: string | null | undefined;
    search?: string | null | undefined;
    slashes?: boolean | null | undefined;
    port?: string | number | null | undefined;
    query?: string | null | ParsedUrlQueryInput | undefined;
}

type Url = string | UrlObject;
```

#### What exactly is causing the error?

If we cross-compare all 14 keys within the `InternalLinkProps` type with all keys defined within the `React.AnchorHTMLAttributes<HTMLAnchorElement>` entity (which `Omit` is acting on), we find that only *4 out of 14 keys* in `keyof InternalLinkProps` match keys within the targeted `React.AnchorHTMLAttributes<HTMLAnchorElement>` type. 

The implication? 

The `Omit` type helper fails to detect that 10 out of 14 keys in the `keyof InternalLinkProps` argument do not exist at all in the `React.AnchorHTMLAttributes<HTMLAnchorElement>`. The `RemoveFields` helper does detect the presence of extraneous keys which alerts us of the mismatch to begin with

With that out of the way, we can rewrite our `TargetedLinkProps` typedef as follows (only the four matching keys being passed in as arguments)

```ts
type TargetedLinkProps = RemoveFields<
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
  "href" | "onClick" | "onMouseEnter" | "onTouchStart"
> &
  InternalLinkProps & {
    children?: React.ReactNode;
  } & React.RefAttributes<HTMLAnchorElement>;
```


### Extractor Types

There are a number of commonly used helper types that unwrap or extract internal types by utilizing TypeScripts `infer` method

For example

```ts
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Unenumerate<T> = T extends Array<infer U> ? U : T;
```

In fact, we could have taken this `extractor` route to derive the types contained within the Nextjs `Link` Component in the previous section instead of eyeballing it and manually pulling the types out

```ts
import Link from "next/link";

type InferReactForwardRefExoticComponentProps<T> = T extends 
  React.ForwardRefExoticComponent<infer U> 
   ? U 
   : T;


/**
 * this is equal to the previously defined 
 * `TargetedLinkProps` type
 */

type ExtractedLinkProps = 
InferReactForwardRefExoticComponentProps<typeof Link>

```

#### The `ExtractPropsTargeted` Extractor Type

This is the second of five core typedefs defined in the very first code block in this post

```ts
type ExtractPropsTargeted<T> = T extends 
  React.DetailedHTMLProps<infer U, Element>
  ? U
  : T;
```

For context, the `React.DetailedHTMLProps` type has the following definition

```ts
type DetailedHTMLProps<E extends React.HTMLAttributes<T>, T> =
 React.ClassAttributes<T> & E
```

In practice, this looks something like

```ts
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
```

So what is it that we're trying to pull out exactly? We are targeting the first of two generic arguments within `React.DetailedHTMLProps` which I usually refer to as the `attribute<element>` tandem

#### Using `ExtractPropsTargeted` within the `PropsTargeted` Mapper definition

The Second type, `ExtractPropsTargeted`, is used to extract all attribute-element tandems that exist in React from the recursively mapped `globalThis.JSX.IntrinsicElements` entity. This entity contains key-value pairs that each have an outer `React.DetailedHTMLProps` wrapper


![IntrinsicElements](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8tggd9yqw69z1uj7l9q1.png)

As illustrated below, the `ExtractPropsTargeted` Extractor type wraps the `globalThis.JSX.IntrinsicElements` entity to effectively derive each `Attribute<Element>` tandem. The exact tandem pulled out is a function of the key passed in ("div", "a", "p", etc.). 

```ts

type PropsTargeted<
  T extends keyof globalThis.JSX.IntrinsicElements = 
  keyof globalThis.JSX.IntrinsicElements
> = {
  [P in T]: ExtractPropsTargeted<
    globalThis.JSX.IntrinsicElements[P]
  >;
}[T];

```

A simple use case for this type is as follows

```tsx
export function TextField({
  id,
  label,
  type = "text",
  className,
  ...props
}: PropsTargeted<"input"> & { label: ReactNode }) {
  return (
    <div className={className}>
      {label && <Label htmlFor={id}>{label}</Label>}
      <input id={id} type={type} {...props} className={formClasses} />
    </div>
  );
}
```

Intellisense when hovering above the spread `...props`


![PropsTargeted](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ee0c7nt6nsdepxu0v55r.png)


### The Payoff — `PropsExcludeTargeted` and `PropsIncludeTargeted`

At last we arrive at the payoff. The first three core types (`RemoveFields`, `ExtractPropsTargeted`, and `PropsTargeted`) have provided the workup necessary for writing these truly intuitive helper types

![Nods](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gvonrzbt3x4alnaljb2p.gif)

The final two of the initial five core types contained within the first code block of this article each take two arguments; (1) the targeted intrinsic element key such as `"div"` or `"svg"` or `"li"` etc and (2) the keys of targeted fields within a given top-level intrinsic element (as defined by (1)). The second argument, a union of keys, serves to either exclude or include targeted fields appropriately.    

#### The `PropsExcludeTargeted` type

The definition of the `PropsExcludeTargeted` type is as follows

```ts
type PropsExcludeTargeted<
  T extends keyof globalThis.JSX.IntrinsicElements = 
  keyof globalThis.JSX.IntrinsicElements,
  J extends keyof PropsTargeted<T> = 
  keyof PropsTargeted<T>
> = RemoveFields<PropsTargeted<T>, J>;
```

This is used in practice for example when you have an `svg` with `viewBox`, `xmlns`, and `aria-hidden` defined in the original component. There's no sense in forwarding props that will do absolutely nothing if populated in a parent component elsewhere.

Therefore, best practice is to preemptively strip known static or unchanging fields from the originating component as follows

```tsx
const AppleIcon: FC<
  PropsExcludeTargeted<"svg", "viewBox" | "xmlns">
> = ({ fill, ...props }) => (
  <svg
    {...props}
    viewBox='0 0 815 1000'
    fill='none'
    xmlns='http://www.w3.org/2000/svg'>
    <path
      fill={fill ?? "black"}
      d='M788.1 340.9C782.3 345.4 679.9 403.1 679.9 531.4C679.9 679.8 810.2 732.3 814.1 733.6C813.5 736.8 793.4 805.5 745.4 875.5C702.6 937.1 657.9 998.6 589.9 998.6C521.9 998.6 504.4 959.1 425.9 959.1C349.4 959.1 322.2 999.9 260 999.9C197.8 999.9 154.4 942.9 104.5 872.9C46.7 790.7 0 663 0 541.8C0 347.4 126.4 244.3 250.8 244.3C316.9 244.3 372 287.7 413.5 287.7C453 287.7 514.6 241.7 589.8 241.7C618.3 241.7 720.7 244.3 788.1 340.9ZM554.1 159.4C585.2 122.5 607.2 71.3 607.2 20.1C607.2 13 606.6 5.8 605.3 0C554.7 1.9 494.5 33.7 458.2 75.8C429.7 108.2 403.1 159.4 403.1 211.3C403.1 219.1 404.4 226.9 405 229.4C408.2 230 413.4 230.7 418.6 230.7C464 230.7 521.1 200.3 554.1 159.4Z'
    />
  </svg>
);

```

While `fill` is defined as `none` in the top level `svg` intrinsic element, you can always pass its prop down into a nested intrinsic element if the opportunity presents itself instead of outright omitting it (such as `path` in this situation). The `fill` prop is of type `string | undefined` in `svg` and `path` alike. 


#### My Personal Favorite — `PropsIncludeTargeted`

The `PropsIncludeTargeted` type is defined as follows

```ts
type PropsIncludeTargeted<
  T extends keyof globalThis.JSX.IntrinsicElements = 
  keyof globalThis.JSX.IntrinsicElements,
  J extends keyof PropsTargeted<T> = 
  keyof PropsTargeted<T>
> = RemoveFields<
      PropsTargeted<T>, 
      Exclude<keyof PropsTargeted<T>, J>
    >;
```

Why is this type my personal favorite of the five? Because WYSIWYG. The keys passed in to the second argument of the type are the only fields defined — nothing less, nothing more. 


![refreshing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3twqk5cazexnjxs9ybg.gif)

Consider the following simple example with a reusable Label component

```tsx
function Label({
  htmlFor: id,
  children
}: PropsIncludeTargeted<"label", "htmlFor" | "children">) {
  return (
    <label
      htmlFor={id}
      className='mb-2 block text-sm font-semibold text-gray-900'>
      {children}
    </label>
  );
}
```

The _only_ two fields derived from the targeted `LabelHTMLAttributes<HTMLLabelElement>` tandem are `children` and `htmlFor` as is expected by the key arguments passed in. This results in the following inferred type def

```ts
{
    htmlFor?: string | undefined;
    children?: ReactNode;
}
```


### Wrapping it up

This post is the first of ? in a series of posts about how utilizing typescript and generics can streamline and clean up code. In future articles I intend to cover utilizing generics in other contexts such as the filesystem, the api layer, and more. Thanks for reading along, cheers


![Wait that is illegal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/txzyptufxyl1ywnvwvdg.png)