如何基于泛型类型转换功能组件内部的值

How to convert value inside functional component based on generic type

我有以下组件:

// ...

type StringOrNumber = string | number;

type InputProps<T extends StringOrNumber> = {
  value: T;
  onSubmit: (value: T) => void;
};

export default function Input<T extends StringOrNumber>(props: InputProps<T>) {
  const [value, setValue] = useState(props.value.toString());

  // Called on enter & blur
  const submitValue = () => {
    //  ts(2345): Argument of type 'string' is not assignable to parameter of type 'T'.
    props.onSubmit(value);
  };

  // ...

  return (
    <input
      {/* ... */}
      onChange={(e) => setValue(e.target.value)}
      value={value}
    />
  );
}

上面 submitValue 中的错误原因很明显,因为 value 在这里总是类型 string,而 onSubmit 期望 stringnumber,具体取决于组件的通用 T

问题是,我不知道如何正确解决这个错误。我尝试使用类型保护,但导致了同样的错误:

function isNumber(x: StringOrNumber): x is number {
  return typeof x === 'number';
}

// ...

export default function Input<T extends StringOrNumber>(props: InputProps<T>): JSX.Element {
  // ...
  const submitValue = () => {
    if (isNumber(props.value)) {
      const numberValue = parseFloat(value) || 0;
      //  ts(2345): Argument of type 'number' is not assignable to parameter of type 'T'.
      props.onSubmit(numberValue);
    } else {
      //  ts(2345): Argument of type 'string' is not assignable to parameter of type 'T'.
    props.onSubmit(value);
    }
  };
  // ...
}

我想问题是类型保护只在这里检查 props.value 的类型,而它应该以某种方式检查 onSubmit 实际期望的参数类型。

这是如何正确完成的?

问题是 extends StringOrNumber 其中 StringOrNumberstring | number 允许 stringnumber 子类型 以及这两种类型。字符串文字类型是 string 的子类型,数字文字类型是 number 的子类型。因此,即使您转换为 string,您也可能不满足 T 上的约束,后者可能比 T 更具体(例如,字符串文字类型 "example")。

您的组件不能在一般情况下基于通用类型参数正确转换。那么,三个选项供您选择:

  1. 一个相当实用但技术上不正确的类型断言。

  2. 也接受道具中的转换功能

  3. 使用有区别的联合而不是泛型,所以总是恰好有两种可能性:stringnumber

一个相当实用但技术上不正确的类型断言

#1 看起来像这样,但在技术上又是不正确的:

// Called on enter & blur
const submitValue = () => {
    if (typeof props.value === "string") {
        props.onSubmit(String(value) as T);
    } else {
        props.onSubmit((parseFloat(value) || 0) as T);
    }
};

Playground link

接受转换函数

#2 看起来像这样:

type InputProps<T extends StringOrNumber> = {
    value: T;
    onSubmit: (value: T) => void;
    convert: (value: string) => T;
};

// ...and then when calling `onSubmit`...

const submitValue = () => {
    props.onSubmit(props.convert(value));
};

Playground link

(或者您可以让 onSubmit 接受 string 并在内部进行转换。)

使用有区别的联合

#3 看起来像这样:

type InputProps =
    {
        __type__: "string",
        value: string;
        onSubmit: (value: string) => void;
    }
    |
    {
        __type__: "number",
        value: number;
        onSubmit: (value: number) => void;
    };

// ...and the component function wouldn't be generic anymore:
export default function Input(props: InputProps) {
// ...

// ...and then when calling `onSubmit`...

const submitValue = () => {
    if (props.__type__ === "string") {
        props.onSubmit(String(value));
    } else {
        props.onSubmit((parseFloat(value) || 0));
    }
};

Playground link