Vadorequest
Vadorequest

Reputation: 17997

React/TypeScript - Extending component with generic type and reuse generic type within component

I have the following component that takes a generic node and patchCurrentNode props.

import { css } from '@emotion/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, {
  PropsWithChildren,
  useState,
} from 'react';
import BaseNodeData from '../../types/BaseNodeData';
import { PatchCurrentNode } from '../../types/BaseNodeProps';

type Props<NodeData extends BaseNodeData = BaseNodeData> = {
  node: NodeData;
  patchCurrentNode: PatchCurrentNode<NodeData>;
};

/**
 * Input that stores the name of the node's selected value.
 *
 * Some nodes ask for an input (text, choice, etc.), the selected value will be stored and be indexed using the selected variable's name.
 */
export const VariableNameInput: <NodeData extends BaseNodeData = BaseNodeData>(p: PropsWithChildren<Props<NodeData>>) => React.ReactElement = (props) => {
  const {
    node,
    patchCurrentNode,
  } = props;
  const {
    width = 200,

  } = node;
  console.log('node', node);
  const [variableName, setVariableName] = useState<string | undefined>(node?.data?.variableName);
  console.log('VariableNameInput variableName', variableName);

  const onChange = (event: any) => {
    console.log('onChange variableName', event, event?.target?.value);
    setVariableName(event?.target?.value);
  };

  const onSubmit = () => {
    console.log('onSubmit variableName', variableName);
    patchCurrentNode({
      data: {
        variableName: variableName,
      },
    });
  };

  return (
    <div
      className={'variable-name-container'}
      css={css`
        position: absolute;
        bottom: 0;
        background-color: black;
        width: ${width}px;
        height: 50px;
        margin-left: -15px;
        margin-bottom: -15px;
        padding-left: 15px;
        border-radius: 5px;

        .variable-name {
          width: ${width - 50}px;
          margin-top: 12px;
          margin-left: -5px;
          padding-left: 5px;
          background-color: black;
          color: ${variableName?.length ? 'white' : '#6E6E6E'}; // Change different color between placeholder and actual value
          border: 1px solid #6E6E6E;
        }

        .submit {
          color: #6E6E6E;
          margin-left: 10px;
          cursor: pointer;
        }
      `}
    >
      <input
        className={'variable-name'}
        placeholder={'Variable name'}
        value={variableName}
        onChange={onChange}
      />

      <FontAwesomeIcon
        className={'submit'}
        icon={['fas', 'paper-plane']}
        onClick={onSubmit}
      />
    </div>
  );
};

export default VariableNameInput;

It is used like this:

<VariableNameInput node={node} patchCurrentNode={patchCurrentNode} />

With the following types:

import BaseNodeData from '../BaseNodeData';
import { QuestionNodeAdditionalData } from './QuestionNodeAdditionalData';

export type QuestionNodeData = BaseNodeData<QuestionNodeAdditionalData>;

---

import BaseNodeAdditionalData from '../BaseNodeAdditionalData';
import { QuestionChoiceType } from './QuestionChoiceType';

export type QuestionNodeAdditionalData = BaseNodeAdditionalData & {
  text?: string;
  questionType?: QuestionChoiceType;
  variableName?: string;
};

The implementation of the generic in the component works. But, I still have TS errors: enter image description here

enter image description here


How can I reuse the generic NodeData type in the component's implementation?

I was thinking I could do something like node?.data?.variableName as NodeData, but NodeData isn't known there.

enter image description here


Edit: Solution

Upvotes: 0

Views: 1441

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42170

You aren't getting access to the generic NodeData inside of the component body due to how you have defined the component's type signature separately from it's implementation.

export const VariableNameInput: <NodeData extends BaseNodeData = BaseNodeData>(
  p: PropsWithChildren<Props<NodeData>>
) => React.ReactElement = (props) => {
  // ...
};

Instead of applying the type annotations to the VariableNameInput variable, apply them directly to the function arguments and return type.

export const VariableNameInput = <NodeData extends BaseNodeData = BaseNodeData>(
  props: PropsWithChildren<Props<NodeData>>
): React.ReactElement => {
  // ...
};

You are now able to access NodeData inside the component.

useState<string | undefined>((node as NodeData).data?.variableName);

But you don't get any additional information by asserting this since node is a required prop and it is always type NodeData.

It seems more likely that you need to define variableName as an optional string prop on the BaseNodeAdditionalData type. If you edit your post to include the types for BaseNodeData and PatchCurrentNode (and their dependencies) then perhaps I can help you fix the underlying issues.

It's the callback patchCurrentNode that is going to give you the most trouble because NodeData could extend BaseNodeData by requiring additional properties or more specific values. You can assert as Partial<NodeData>, but you want to be confident that any assertion you make is actually correct.

Typescript Playground Link

Upvotes: 3

Related Questions