如何在使用自定义React组件时不丢失Typecript类型

179 阅读3分钟

如果我们不小心,在实现自定义React组件时,我们会失去Typescript类型。当实现像表单控件这样的普通组件时,这就变得特别有问题。在这篇文章中,我们将在我们的应用程序中使用一个普通的组件(一个无线电组),并修复其类型,以确保我们提供的任何值和回调都是正确的类型。

一个有问题的组件。无线电组

假设我们的应用程序中有一个常见的RadioGroup 组件。我们有一堆表单,不想重复创建广播组,所以这有很大的意义。我们最初的(有问题的)无线电组的实现如下。

type RadioGroupProps = {
  radios: { label: string; value: string }[];
  checked: string;
  setChecked: (value: string) => void;
};

const RadioGroup = ({ radios, checked, setChecked }: RadioGroupProps) => {
  return (
    <fieldset>
      {radios.map(({ label, value }) => (
        <label>
          <input
            type="radio"
            checked={value === checked}
            onChange={() => setChecked(value)}
          />
          {label}
        </label>
      ))}
    </fieldset>
  );
};

从表面上看,这似乎很好,它将迭代我们提供的无线电选项列表,并根据我们提供的道具显示checked 无线电。然而,当我们试图实现它时,问题就变得非常明显了。

以这个实现为例,我们让用户选择他们是有汽车还是摩托车。

type Vehicle = 'car' | 'motorcycle';

const radios: { label: string; value: Vehicle }[] = [
  { label: 'Car', value: 'car' },
  { label: 'Motorcycle', value: 'motorcycle' },
];

export default function App() {
  const [checked, setChecked] = useState<Vehicle>('car');

  return (
    <div className="App">
      <RadioGroup
        radios={radios}
        //type error on setChecked!
        setChecked={setChecked}
        checked={checked}
      />
    </div>
  );
}

我们发现我们在setChecked !上有一个类型错误。如果我们看一下编译器的信息,我们会看到以下内容。

类型'Dispatch<SetStateAction>'不能分配给类型'(value: string) => void'参数的类型'value'和'value'不兼容。 类型'string'不能分配给类型'SetStateAction'

那么这里发生了什么?原来我们只是告诉我们的普通组件RadioGroupradios 数组中包含的值是strings ,反过来,setChecked 函数将被调用为string 。这是一个问题:我们从父组件传递的setChecked 函数期望的类型比string 更严格:它期望的是Vehicle

但是,当我们的RadioGroup 必须能够接受任何其他的值类型时,它怎么能知道Vehicle 的类型呢?

糟糕的 "修复"。类型断言

我见过的一个糟糕的 "修复 "方法是使用一个类型断言来告诉Typescript我们知道我们传递给setChecked 的类型确实是一个Vehicle 。它将会是这样的。

export default function App() {
  const [checked, setChecked] = useState<Vehicle>('car');

  return (
    <div className="App">
      <RadioGroup
        radios={radios}
        setChecked={(value) => setChecked(value as Vehicle)}
        checked={checked}
      />
    </div>
  );
}

这样做是可以的,但真正的意义是什么呢?虽然使用类型断言有时是必要的,但我们应该尽量避免:告诉编译器我们比它更清楚,当我们错了的时候,就会导致运行时错误!所以我们如何正确地解决这个问题?

那么,我们如何以正确的方式解决这个问题呢?

好的解决方法。泛型!

幸运的是,这正是Typescript泛型的完美用例!在下面的代码中,我们使用了一个通用的Textends string 。当我们把我们的车辆radios 传递给RadioGroup 组件时,它现在已经足够聪明,知道泛型T 代表了我们要传递的无线电值的类型。当我们在value 上调用setChecked ,值现在是类型T ,而我们的车辆情况确实是类型Vehicle

type RadioGroupProps<T> = {
  radios: { label: string; value: T }[];
  checked: T;
  setChecked: (value: T) => void;
};

const RadioGroup = <T extends string>({
  radios,
  checked,
  setChecked,
}: RadioGroupProps<T>) => {
  return (
    <fieldset>
      {radios.map(({ label, value }) => (
        <label>
          <input
            type="radio"
            checked={value === checked}
            onChange={() => setChecked(value)}
          />
          {label}
        </label>
      ))}
    </fieldset>
  );
};

type Vehicle = 'car' | 'motorcycle';

const radios: { label: string; value: Vehicle }[] = [
  { label: 'Car', value: 'car' },
  { label: 'Motorcycle', value: 'motorcycle' },
];

export default function App() {
  const [checked, setChecked] = useState<Vehicle>('car');

  return (
    <div className="App">
      <RadioGroup radios={radios} setChecked={setChecked} checked={checked} />
    </div>
  );
}

我们就可以开始了:我们的RadioGroup 现在将支持我们使用的任何无线电类型!