跳到主要内容

在 TypeScript 中写高阶组件

· 阅读需 4 分钟
Chen Siwei

高阶组件(Higher-Order Component, HOC) 是 React 中的一种高级技巧。但是在 TypeScript 中写高阶组件可能会遇到一点障碍。

高阶组件

如果你忘了或还未了解过高阶组件,下面是个最简单的例子:

const withBorder = (Component, color) => {
return (props) => {
return (
<div style={{ border: `solid 1px ${color}` }}>
<Component {...props} />
</div>
);
};
};

React 中约定高阶组件名应当以 'with' 开头。

这是一个能给任何组件加上边框的高阶函数,它的返回值也是个组件。比如我们给下面的 Welcome 加上红色边框,则可以:

const Welcome = ({ name }) => {
return <div>Hello {name}!</div>;
};

// 带有红色边框的 Welcome
const RedBorderedWelcome = withBorder(Welcome, 'red');

const View = () => <RedBorderedWelcome name="Talaxy" />;

TypeScript 中的高阶组件

可以在 TypeScript 中这么写上面的 withBorder 高阶组件:

// 如果在 function 声明中定义范型,则可以不需要 `extends {}`
const withBorder = <T extends {}>(
Component: ComponentType<T>,
color: string,
) => {
return (props: T) => {
return (
<div style={{ border: `solid 1px ${color}` }}>
<Component {...props} />
</div>
);
};
};
  • 我们需要定一个范型 T(且限定为空对象的扩展),指代传入组件的属性类型;

  • 在返回的组件中,需要标注上 props 的类型为 T

稍微...复杂一点?

其实写这篇之初,是我在实际项目中给页面加 AB 测试遇到了困难。

项目中的 AB 测试是从 API 请求的,其实在页面里就可以做这个请求,但是考虑到别的一些页面也会用的 AB 测试,因此我想到把 AB 测试写在高阶组件里:

function withABTest<T>(
Component: ComponentType<T>,
category: ABTestCategory, // 枚举类型,指定实际需求里 AB 测试的类别
) {
return (props: T) => {
const [testCase, setTestCase] = useState<string>();

useEffect(() => {
fetchABTest(category).then(setTestCase);
}, []);

if (!testCase) return null;
return <Component {...props} testCase={testCase} />;
};
}

const Page = (props: { testCase: string }) => {
return <div>我是{props.testCase}测试集。</div>;
};

const PageWithABTest = withABTest(Page, ABTestCategory.SomeCategory);

第一眼看上去可能感觉这个高阶组件没什么问题。但实际则是错误地使用了 TypeScript 中的范型:

  • Component 需要 testCase 参数,但未在声明里进行限定;

  • 高阶组件中的 props 应当比 Component 少一个 testCase ,而不是共同指向了 T

因此,我们需要这样定义类型:

function withABTest<P>(
Component: ComponentType<P & { testCase: string }>,
category: ABTestCategory,
) {
return (props: P) => {
const [testCase, setTestCase] = useState<string>();

useEffect(() => {
fetchABTest(category).then(setTestCase);
}, []);

if (!testCase) return null;
return <Component {...props} testCase={testCase} />;
};
}

参考

Higher-Order Components - React