如果我们不小心,在实现自定义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'
那么这里发生了什么?原来我们只是告诉我们的普通组件RadioGroup ,radios 数组中包含的值是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泛型的完美用例!在下面的代码中,我们使用了一个通用的T ,extends 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 现在将支持我们使用的任何无线电类型!