前言
最近在使用 styled-components
时,遇到个让我很难受的事情
目前效果
多个节点虽然在使用上有层级关系的,但是 styled-components
其实是没有相关支持的,只能一个个节点单独声明
import styled from "styled-components";
const Layout = styled.div``;
const LayoutHeader = styled.div``;
const LayoutContent = styled.div``;
const LayoutContentLeft = styled.div``;
const LayoutContentRight = styled.div``;
const LayoutFooter = styled.div``;
function App() {
return (
<Layout>
<LayoutHeader>头部</LayoutHeader>
<LayoutContent>
主体
<LayoutContentLeft>主体的左边</LayoutContentLeft>
<LayoutContentRight>主体的右边</LayoutContentRight>
</LayoutContent>
<LayoutFooter>尾部</LayoutFooter>
</Layout>
);
}
我期望的
我期望组件是有层级结构的,如下:
function App() {
return (
<Layout>
<Layout.Header>头部</Layout.Header>
<Layout.Content>
主体
<Layout.Content.Left>主体的左边</Layout.Content.Left>
<Layout.Content.Right>主体的右边</Layout.Content.Right>
</Layout.Content>
<Layout.Footer>尾部</Layout.Footer>
</Layout>
);
}
是有层级结构的:
- Layout
- Layout.Header
- Layout.Content
- Layout.Content.Left
- Layout.Content.Right
- Layout.Footer
难点分析
把一个平铺结构改成树状结构不是难事,写个 function 处理一下不就好了
难的是如何让转化之后的结果实现 ts 类型推导,也就是根据你输入的内容,提示当前层级有哪些子层级
没错,这片文章我是来教你如何写 typescript 的
完整代码
我们将要实现一个 styledTier
方法来完成这件事
先来看下完整代码
App.tsx
import { styledTier } from "./utils.ts";
/**
* 每一级可以单传一个组件也可以传一个对象
* 如果存在下一层则只能传对象
**/
const Layout = styledTier({
_self: styled.div``,
Header: styled.div``, // 单传一个组件
Content: {
// 传一个对象
_self: styled.div``, // "_self" 属性表示本层组件
Left: styled.div``,
Right: styled.div``,
},
Footer: styled.div``,
});
export default function App() {
return (
<Layout>
<Layout.Header>头部</Layout.Header>
<Layout.Content>
主体
<Layout.Content.Left>主体的左边</Layout.Content.Left>
<Layout.Content.Right>主体的右边</Layout.Content.Right>
</Layout.Content>
<Layout.Footer>尾部</Layout.Footer>
</Layout>
);
}
utils.ts
import { StyledComponent } from "styled-components";
interface IHierarchyInput {
[T: string]: StyledComponent<any, any> | IHierarchyInput;
_self: StyledComponent<any, any>;
}
type IHierarchyOutput<Input extends IHierarchyInput = any> = Input["_self"] & {
[Prop in keyof Omit<Input, "_self">]: Input[Prop] extends IHierarchyInput
? IHierarchyOutput<Input[Prop]>
: Input[Prop];
};
export const styledTier = function <O extends IHierarchyInput>(
input: O
): IHierarchyOutput<O> {
const { _self: component, ...children } = input;
const current: any = component;
for (const [pKey, pItem] of Object.entries(children)) {
if (pItem._self) {
current[pKey] = styledTier(pItem);
} else {
current[pKey] = pItem;
}
}
return current;
};
详细讲解
入参
IHierarchyInput
是用来约束入参的
import { StyledComponent } from "styled-components";
interface IHierarchyInput {
[T: string]: StyledComponent<any, any> | IHierarchyInput;
_self: StyledComponent<any, any>;
}
有三个关键点:
- 属性的定义(
_self
属性是有特殊的类型定义的) - 联合类型(其他属性可以直接传组件,也可以传一个对象)
- 类型递归嵌套
属性的定义
构造一个简单的对象:
// 属性名为 string,属性值为 number
interface MyObj {
[T: string]: number;
}
// 属性名为 number,属性值为 number(其实这样没有意义,因为对象在 js 中属性名都会转成 string)
interface MyObj {
[T: number]: number;
}
// 或者还可以这样(这样只在 ts 中有意义,而在 js 中是没有意义的)
interface MyObj {
[T: string]: number;
[T: number]: number;
}
但是如果我要对其中某个属性做特定的约束呢,譬如下面的属性 before
和 after
:
interface MyTest {
before: string;
[T: string]: number | string | Array<any>;
after: Array<any>;
}
有两点需要注意的:
- 位置没有先后之分,所以上面
before
跟after
实现效果是一致的 [T: string]
对应的类型必须包含before
和after
的类型
联合类型
type StrOrNum = string | number;
const str: StrOrNum = "abc";
const num: StrOrNum = 123;
类型递归嵌套
类型的定义也可以使用递归嵌套的
type MyArr = Array<number | MyArr>;
const arr: MyArr = [1, 2, 3, [4, 5, 6, [7, 8, 9]]];
函数范型
如何才能让函数在调用时根据传入的参数类型来决定其他参数或者返回值类型捏
这就要用到范型了,基本用法长这样:
function fn<T>(){}
举个简单的例子:
// 定义范型 T,分别用于两个参数的不同位置,用以确定这两个位置最终类型会是一致
function definition<T>(input: T, callback: (result: T) => void) {
callback(input);
}
definition(2, function (result) {
console.log(result); // 提示为 number
});
definition("i'm sb", function (result) {
console.log(result); // 提示为 string
});
返回值
IHierarchyOutput
是用来约束返回值的
type IHierarchyOutput<Input extends IHierarchyInput = any> = Input["_self"] & {
[Prop in keyof Omit<Input, "_self">]: Input[Prop] extends IHierarchyInput
? IHierarchyOutput<Input[Prop]>
: Input[Prop];
};
首先,有以下几个知识点:
- 范型
- 约束范型的类型(通过
extends
来约束范型只能是某些类型) - 属性的定义(通过
in
和keyof
遍历别的类型来构造一个对象) - 类型判断(通过
extends
还有三目运算符来实现类型的判断和转化) - 递归嵌套结构
范型
这里不过多陈述了,都是基础,举个简单的例子吧:
// 定义一个有范型的类型
type MyTuple<A, B> = [A, B];
const t1: MyTuple<string, number> = ["abc", 123];
const t2: MyTuple<number, number> = [456, 123];
约束范型的类型
extends
关键字字面意思是“继承”,同时他也可以用来约束范型必须是什么类型,譬如
// A 必须是 string 或者是 number,而 B 必须是 number
type MyTuple<A extends string | number, B extends number> = [A, B];
const t1: MyTuple<number, number> = [456, 123];
const t2: MyTuple<boolean, number> = [false, 123]; // 报错
const t2: MyTuple<number, string> = [456, "abc"]; // 报错
属性的定义
关于 in
和 keyof
的详细介绍可以看我这篇文章,Typescript 关键字
类型判断
关于 extends
的详细介绍可以看我这篇文章,Typescript 关键字
递归嵌套结构
type IHierarchyOutput<Input extends IHierarchyInput = any> = Input["_self"] & {
// 排除 _self
[Prop in keyof Omit<Input, "_self">]: Input[Prop] extends IHierarchyInput // 判断当前属性值是否还是对象结构
? IHierarchyOutput<Input[Prop]> // 如果是,则把当前属性值作为范型重新传入 IHierarchyOutput 中,开启下一轮循环
: Input[Prop];
};