Using react cloneElement to over-ride prop type

Viewed 28

I have this TextField component, which is input form that takes in string and using React.cloneElement, I'm trying to create NumberField, which is another input form, but takes in number type. There are two props value and onChange from TextField that I would like to change from string to number as I use React.cloneElement to copy over, but I'm getting type errors on e.currentTarget.value from NumberField saying "Argument of type 'string' is not assignable to parameter of type 'number'." I also get another error in App.js on e.target.value saying "Property 'target' does not exist on type 'number'." All I'm trying to do is use cloneElement to over-ride prop types from string to number, but it's proving difficult. What am I doing wrong? https://codesandbox.io/s/over-ride-prop-type-mjw1sp?file=/src/App.tsx

TextField.tsx

import React from "react";
import "./styles.css";

export interface ITextField {
  onChange?: (value: string) => void;
  value?: string;
}

export const TextField: React.FC<ITextField> = ({ value = "", onChange }) => {
  const onChangeValue = (value: string) => {
    if (typeof onChange === "function") {
      onChange(value);
    }
  };

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(e: React.FormEvent<HTMLInputElement>) =>
          onChangeValue(e.currentTarget.value)
        }
      />
    </div>
  );
};

NumberField.tsx

import React, { useState } from "react";
import "./styles.css";
import { TextField, ITextField } from "./TextField";

export interface INumberField extends Omit<ITextField, "onChange" | "value"> {
  onChange?: (value: number) => void;
  value: number;
  formatOnType?: boolean;
}

export const NumberField: React.FC<INumberField> = ({ value, onChange }) => {
  const [numberValue, setNumberValue] = useState<number>(0);
  const onChangeValue = (value: number) => {
    if (typeof onChange === "function") {
      onChange(value);
    }
  };

  return (
    <>
      {React.cloneElement(<TextField />, {
        value: numberValue,
        onChange: (e: React.FormEvent<HTMLInputElement>) =>
          onChangeValue(e.currentTarget.value)
      })}
    </>
  );
};

App.tsx

import React, { useState } from "react";
import { NumberField } from "./NumberField";
import "./styles.css";

export default function App() {
  const [value, setValue] = useState(0);

  return (
    <div>
      <NumberField value={value} onChange={(e) => setValue(e.target.value)} />
    </div>
  );
}
1 Answers

In HTML inputs (and in react inputs), for values are strings.React.FormEvent<HTMLInputElement> always return a string. Now I understand you want to store a number. With a standalone component, that would give the following:

export interface INumberField extends Omit<ITextField, "onChange" | "value"> {
  onChange?: (value: number) => void;
  value: number;
  formatOnType?: boolean;
}

export const NumberField = ({ value, onChange }: INumberField) => {
  // the input typed by the user is always a string
  const [value, setValue] = useState<string>('0');

  const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
      const numberValue = parseInt(e.target.value, 10);
      onChange(numberValue)
      setValue(e.target.value);
    }
  };

  return (
    <div>
      <input
        type="text"
        {/* automatically converted to string */}
        value={value}
        onChange={handleChange}
      />
    </div>
   );
};

As for reusing a TextField to build a NumberField, I'm afraid you'll be fighting (and losing) against TypeScript. The right way to do it would be to write a generic input, and to specialize it by type.

export interface IField<T> {
  onChange: (value: T) => void;
  value: T;
  convert?: (value: string) => T;
}

export const Field = <T>({ value, onChange, convert = x => x }: IField<T>) => {{
  // the input typed by the user is always a string
  const [value, setValue] = useState<string>('0');

  const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
      const parsedValue = convert(e.target.value);
      onChange(parsedValue)
      setNumberValue(e.target.value);
    }
  };

  return (
    <div>
      <input
        type="text"
        {/* automatically converted to string */}
        value={value}
        onChange={handleChange}
      />
    </div>
   );
};

Then your fields are:

export interface ITextField extends IField<string> {}
const TextField = (props: ITextField) => <Field<string> {...props} /> 

export interface INumberField extends IField<number> {}
const NumberField = ({ value, onChange }: INumberField) => (
    <Field<number> value={value} onChange={onChange} convert={v => parseInt(v, 10)} /> 
);
Related