0

When an interface exposes a generic that simply get's used to type one the properties. Is there a way to "use" that based on inference at a usecase?

Take a look at this:

enter image description here

please assume here that the Generic cannot be simply applied, and that the onClick will exist.

As you can see, the is property of my TestObject is a generic, that is statically given. Is there a way to put bounds around this so when the onClick wants an argument, it knows that the is property is div to therefore only allow value == 'div'.

My use case for this is in the React world where I want my component to be given a prop that defines its render (createElement), but need that to be typesafe for all handlers and attributes it applies. I suppose a generic would work, but that falls apart when sent into forwardRef.

Here is an example of what I have currently, and also where my predicament lies.

import { AllHTMLAttributes, createElement, forwardRef } from 'react';

interface Props<Element extends keyof JSX.IntrinsicElements>
    extends Omit<AllHTMLAttributes<Element>, 'width' | 'height'> {
    is?: Element;
    className?: string;
}

// There is a little more going on inside the Component, but you get the gist.
const Box = forwardRef<HTMLElement, Props<'div'>>(({ is, children }, ref) =>
    createElement(is, {
        ref,
    }, children));

As you can see from there, now the is prop is locked to just being a div.

Marais Rossouw
  • 937
  • 1
  • 10
  • 29

1 Answers1

1

TS currently doesn't support arbitrary generic value types except generic functions. Also, in a variable assignment like const x, the compiler cannot infer the type argument for T automatically.

In other words, you have to give TestObject a concrete type argument: const x: TestObject<"div">. Your case still compiles, as the given default "div"|"a" for T is used, when nothing is specified. Alternatively you could use a factory function to initializex, but here I would just go with the former for simplicity.


The issue with React.forwardRef is related to above topic, albeit a bit more complex.

React.forwardRef cannot output a generic component with current React type definitions - I have mentioned some workarounds in the linked answer. The simplest workaround for you is to use a type assertion:

const Box = forwardRef<HTMLElement, Props<keyof JSX.IntrinsicElements>>(({ is, children }, ref) =>
  is === undefined ? null : createElement(is, { ref, }, children)) as
  <T extends keyof JSX.IntrinsicElements>(p: Props<T> &
  { ref?: Ref<HTMLElementFrom<T>> }) => ReactElement | null

// this is just a helper to get the corresponding HTMLElement, e.g. "a" -> HTMLAnchorElement
type HTMLElementFrom<K extends keyof JSX.IntrinsicElements> = 
  NonNullable<Extract<JSX.IntrinsicElements[K]["ref"], React.RefObject<any>>["current"]>
type AnchorEle = HTMLElementFrom<"a"> // HTMLAnchorElement

This will make your Box generic and you could create both div and a Boxes:

const aRef = React.createRef<HTMLAnchorElement>()
const jsx1 = <Box is="a" ref={aRef} onClick={e =>{}} />
// is?: "a" | undefined, ref: RefObject<HTMLAnchorElement>, onClick?: "a" callback

const divRef = React.createRef<HTMLDivElement>()
const jsx2 = <Box is="div" ref={divRef} onClick={e =>{}} />
// is?: "div" | undefined, ref: React.RefObject<HTMLDivElement>, onClick?: "div" callback

Sample

ford04
  • 66,267
  • 20
  • 199
  • 171