高阶组件(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} />;
    };
}
