attributes with react and typescript.md

If you use typescript with react you may wonder what the right signature is for your “properties” interface.

Obviously, your component will have few to many specific properties that reflect your domain. However, you may wish to also provide for the ability to add other attributes, such as those valid for a div property. In other words, you want to extend your props interface with that of the properties from a div element. How do you do that?

There are really two issues:

  1. How do you extend your props interfaces
  2. How do you filter the props once you receive them to just those that apply.

Extend Your Prop Interface or Use Sum Types

It’s not uncommon to see

interface Props {
 className?: string
 yourProp?: number
}

with the idea of using it in class MyComponent extends React.Component<Props, any> somewhere. If your component will have an outer div, you may also pass in a react element so that its configurable. But let’s say your outer element is a div and you want to be able to allow clients to add any div property to your props and pass that through.

You could just add:

interface Props {
  divProps: Record<string, any>
  yourProp?: number

and now your className and other properties, such as style, should be placed into divProps. However, divProps now allows any property, not just those that are valid on a div. Since interfaces allow you to add properties and still confirm to the interface you could just literally allow them to be added. However, since they have no types, those properties may not have the proper types, blowing away your type safety.

You need an expression that restricts the properties to just those allowed on a div. We can look at the @types for react:

 interface Attributes {
        key?: Key;
    }
    interface ClassAttributes<T> extends Attributes {
        ref?: Ref<T>;
    }

These definitions are in the react type definitions. You can see that adding key and ref, standard react element properties, appear in these definitions. It’s also clear why an element will need to appear for T in order to keep the ref callback type safe. We also do not see any children in these definitions. In many cases, the children property is added to the element type definition e.g. P & { children?: ReactNode} in order to combine your Props with something that has children. We need this all together.

React.Prop is not deprecated so…

    interface HTMLProps<T> extends AllHTMLAttributes<T>, ClassAttributes<T> {
    }

    type DetailedHTMLProps<E extends HTMLAttributes<T>, T> = ClassAttributes<T> & E;

    interface SVGProps<T> extends SVGAttributes<T>, ClassAttributes<T> {
    }

    interface DOMAttributes<T> {
        children?: ReactNode;
        dangerouslySetInnerHTML?: {
            __html: string;
        };
...
    }
    interface HTMLAttributes<T> extends DOMAttributes<T> {
        // React-specific Attributes
        defaultChecked?: boolean;
...
    }
    ...lots of interface definitions that extend HTMLAttributes...but no "DivAttributes"...
}
declare global {
 namespace JSX {
   interface IntrinsicAttributes extends React.Attributes { }
   interface IntrinsicClassAttributes<T> extends React.ClassAttributes<T> { }
...
}}

Here we see that there are few different sets of interfaces for attributes. The DOM attribute allows setting many events as well such as onClick or on onBlur. In JSX, there are some extensions to those class. The complexity of these interfaces all some slicing and dicing of the react types more possible.

The reason that you don’t see a specific DivHTMLAttributes is that div takes the standard HTMLAttributes. Many other elements are like this as well. There is a of course a DOM HTMLDivElement.

So the guidance is now pretty clear if you want to use this approach to have the utmost type saftey on your props:

  • You need ref, key: declare your interface extends ClassAttribute<MyComponent>.
  • You only need children: declare your interface extends DOMAttribute<MyComponent>.
  • You need ref, key, children: declare your interface extends HTMLAttributes<MyComponent>.
  • You want to extend from a more specific HTMLAttributes: declare your interface extends TextAreaHTMLAttributes<MyComponent>.
  • You want react (ref,key) and specific attributes: declare your interface extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>.

The last is quite a bit of typing but it includes ref, key, children and all the HTMLAttributes (which is what is allowed for div).

There is a downside to all of this, you can’t put your Props in the same file as your class because typescript cannot see your component type until you declare it but you are declaring your props that requires your component class as a type argument. Catch-22. You will need to create a separate props declarations file. You only need the separate file if you want your ref callback to be strongly typed.

As an alternative, you may want to just use sum types but that means you would need to use the sum type when you create the bag of properties you are passing in inside your calling code. You should note that most of the declarations for React.Component and similar such as SFC, use the sum type so you can automatically include them when you declare your class.

class MyComponent extends React.Component<Props & ClassAttributes<MyComponent>, any> {
...
}
...
// if you want to declare the type of your props
const props: Props & ClassAttributes<Button> = { ... }
// or not
const props = { ... }

So the moral of the story is…

If you want your props type checked like above, then you have to use the sum type everytime or declare your props to extend other props. You have to choose whether you haev react properties like ref and key or just HTML properties

If you do not care about your props interface being type checked on react properties such as key and ref, then you can skip extending your interface altogether because they are included in the React.Component definition already. You may encounter issues when you apply those properties to your element, but that’s the trade-off.

You will need to choose the best option for you.

Using It

To separate out the properties, you can do some slightly repititive typing:

render() {
  const { yourProp, ...rest } = this.props
  return(<div {...rest}>...</div>)
}

However, this does not scale well. If you have alot of properties coming in, you have list them all in your const destructuring expression. A bit repetitive.

You can also filter them out. Let’s say your using office-ui-fabric-react. This library comes with a filtering function to do exactly that:

import { getNativeProps, divProperties } from "office-ui-fabric-react/lib/Utilities"
render() {
  return(<div {...getNativeProps(this.props, divProperties)}>...</div>)
}

You must have some way to filter them out if you decide to mix them together. The getNativeProps function is nice because it allows you to filter out the props for your div from any interface even if you did not explicitly cast it so.

Comments

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO