使用React来创建使用泛型的组件
我将总结利用泛型来创建组件的方法。当然,我将以功能组件为前提。
使用React组件的范型
首先,让我们给出一个最简单的例子。
type Props<T> = {
data: T
}
const Component = <T,>({ data }: Props<T>) => {
return (
<p>{`これは${typeof(data)}型のコンポーネントです。`}</p>
)
}
关键是在函数的类型泛型中使用<T,>。在TypeScript中,函数的类型泛型通常写作() => {}。但是在React代码中,会被视为标签,所以为了区别开它,我们需要写成<T,>。
如果你想要告诉编译器不是使用标签而是使用泛型,你可以使用extends关键字。如果你想要对T设置类型限制,基本上就使用这种形式。
const Component = <T extends {}>({ data }: Props<T>) => {
// ...
}
当使用此组件时,可以这样做。您还可以明确指定类型参数。
const WrapperComponent: React.FC = () => (
<Component data="文字列" />
)
// これでもOK(型を明示する場合)
const WrapperComponent: React.FC = () => (
<Component<string> data="文字列" />
)
// 明示した型とPropsがマッチしない場合はエラー
const WrapperComponent: React.FC = () => (
<Component<number> data="文字列" /> // これはエラー、dataはnumber型
)
使用泛型的组件进行记忆化
不好的例子
我們將上述組件進行備忘錄化。照常,使用React.memo對組件進行包裝。這樣看起來並不會出現錯誤。
// エラーは発生しないので正しいコードのように見える
const Component = React.memo(<T extends {}>({ data }: Props<T>) => {
// ...
})
Component.displayName = "Component"
const WrappedComponent: React.FC = () => (
<Component data="文字列" />
)
然而,这个例子存在一些陷阱。让我们考虑以下组件。
type Props<T> = {
data: T
arg: T
}
const Component = <T extends {}>({ data, arg }: Props<T>) => {
return (
<p>{`${typeof(data)} ${typeof(arg)}`}</p>
)
}
const MemorizedComponent = React.memo(Component);
MemorizedComponent.displayName = "MemorizedComponent"
const WrappedComponent: React.FC = () => (
// 型が異なるのにエラーが発生しない!
<MemorizedComponent data="文字列" arg={42} />
)
尽管data和arg的类型不同,但并没有引发错误。这是因为当被React.memo包裹时,MemorizedComponent自身并没有使用类型泛型来处理Props。具体而言,MemorizedComponent的类型是React.MemoExoticComponent,对于Component的Props类型并不关心。
const Component = React.memo(({ data }: Props) => {
// …
})
Component.displayName = “Component”// “部分出现错误
// 期望0个类型参数,但实际得到了1个。
const WrappedComponent: React.FC = () => (
<Component data=”文字列” />
)
这也是由于该组件被React.memo包裹所引起的。React.memo不期望泛型作为参数,因此从 TypeScript 的语法角度来说,这是错误的。
正确的例子
通过对记忆化组件进行类型断言,可以解决错误。当然,在实现时需要非常小心地注意类型断言。
type Props<T> = {
data: T
arg: T
}
const Component = <T extends {}>({ data, arg }: Props<T>) => {
return (
<p>{`${typeof(data)} ${typeof(arg)}`}</p>
)
}
// Componentとして型アサーションをする
const MemorizedComponent = React.memo(Component) as typeof Component
// エラーが出る例
const WrappedComponent: React.FC = () => (
// 今度はちゃんとエラーが出る!
<MemorizedComponent data="文字列" arg={42} />
)
// 正しい例
const WrappedComponent: React.FC = () => (
<MemorizedComponent data="文字列" arg="もじれつ" />
)
在这种情况下,我们会将类型断言作为组件的一部分来使用,因此MemorizedComponent的类型将会是({ data, arg }: Props) => React.JSX.Element。通过这样做,我们可以让泛型类型在进行记忆化之前后依然有效。
给它一个displayName
在上述例子中,无法给MemorizedComponent添加displayName属性。
// Property 'displayName' does not exist
MemorizedComponent.displayName = "MemorizedComponent"
由于Component作为类型断言,因此基本上采用上述方法不会引起ESLint的警告。但如果希望添加displayName,则可以按以下方式操作。
const MemorizedComponent = React.memo(Component) as typeof Component & { displayName: string }
MemorizedComponent.displayName = "MemorizedComponent"