0

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

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

6 Comments

Thank you for your explanations, your analysis is correct. I'm unsure whether it is "best" to write the TS type for the function itself, or for the props property directly. I usually do it on the function, but it doesn't seem to work in this case. Here's what I came up with by playing around a bit: export const VariableNameInput: React.FunctionComponent<PropsWithChildren<Props>> = <NodeData extends BaseNodeData = BaseNodeData>(props: PropsWithChildren<Props<NodeData>>): React.ReactElement => {}. But the React.FunctionComponent<PropsWithChildren<Props>> feels duplicated and unnecessary.
Some of it is just preference, but personally I would not apply any type to the component itself. If the function arguments and return are typed then it's not necessary. React.FunctionComponent doesn't work with generic components, so just type the function itself.
Unfortunately, I can't define the variableName in the BaseNodeData, because BaseNodeData is a generic "NodeData" type, which is being augmented by specialized types. I guess in this case my only solution is to create a new NodeDataWithVariableName (or similar) and use it instead of BaseNodeData in the VariableNameInput component.
I think that would be good. If your component expects that you have a node type with a variableName property then you should require that from your props.
Thanks for your assistance Linda, I was able to find a solution. I bet it can be improved/simplified but that'll do for now! github.com/Vadorequest/poc-nextjs-reaflow/commit/…
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.