表示一个函数类型
type Func = (...args: any[]) => any
type Func<T extends any[], R> = (...a: T) => R
// T 是参数,R是函数返回值
一个练习的例子
写出一个函数的重载:
如果没有参数的话,返回一个新的函数,新的函数的参数就是作为返回值
export default function compose(): <R>(a: R) => R
如果有参数的话,只有一个参数的情况下,该参数必须是一个函数,且返回值也是该函数。(参数函数的特点就是:可以传递一个或者多个参数)
export default function compose<F extends Func<unknown[], unknown>>(f: F): F
如果有两个或者多个参数的话,参数们都是函数,且返回值也是函数。返回函数的可以接受一个或者多个参数。且最后一个参数也接受一个或者多个参数。且后面函数的返回值作为前面函数的参数。(
compose(f, g, h)
)=》(...args) => f(g(h(...args)))
export default function compose<A, T extends unknown[], R>(f1: (a: A) => R, f2: Func<T, A>): Func<T, R>
export default function compose<A, B, T extends unknown[], R>(f1: (a: A) => R, f2: (b: B) => A, f3: Func<T, B>): Func<T, R>
同理,四个参数的情况,五个参数的情况就不列举了。
不知道传递几个参数的情况的函数重载。
export default function compose<R>(
f1: (a: unknown) => R,
...funcs: Func<unknown[], unknown>[]
): Func<unknown[], R>
export default function compose<R>(
...func: Func<unknown[], unknown>[]
): Func<unknown[], R>
实现这个函数重载的例子
第一版
export default function compose(...func: Func<unknown[], unknown>[]) {
if (func.length === 0) {
return (...args: unknown[]) => args
}
if (func.length === 1) {
return func[0]
}
return function (...args: unknown[]) {
let result: unknown
for (let i = func.length - 1; i >= 0; i--) {
const fn = func[i]
if (i === func.length - 1) {
result = fn(...args)
} else {
result = fn(result)
}
}
return result
}
}
从第一版实现我们就可以知道:
如果是最后一个函数,可以接受多个参数,中间的函数参数就是以后面一个函数的的返回值来确定的。于是我们相当于就是要实现这样一个函数:compose(f, g, h) => (...args) => f(g(h(...args)))
这种前后的值有关联的操作。我们就能想到用
reduce
给实现以下。第二版
export default function compose(...func: Func<unknown[], unknown>[]) {
if (func.length === 0) {
return (...args: unknown[]) => args
}
if (func.length === 1) {
return func[0]
}
return func.reduce((a, b) => {
return (...args: unknown[]) => {
return a(b(...args))
}
})
// return func.reduce(
// (a, b) =>
// (...args: unknown[]) =>
// a(b(...args))
// )
}
我们来好好理解一下这个
reduce
用一个简化的例子: ```typescript function t1(a: any) { console.log(‘我是t1’) }
function t2(a: any) { console.log(‘我是t2’) } function t3(a: any) { console.log(‘我是t3’) }
const funs = [t1, t2, t3]
funs.reduce((a, b) => { return (…args) => { a(b(…args)) } })
1. 由于`reduce`每一步都是同一个之前函数,所以,最终要求返回一个函数。所以,reducer的回调也肯定是一个函数。这个没问题。
1. 最主要就是后面的函数的返回值作为下一个函数的参数。
1. 每次执行的a和b的值以及reduce的返回值。
| 第几次执行 | a | b | 返回值 |
| --- | --- | --- | --- |
| 1 | t1 | t2 | ```typescript
(...args:any)=> {
t1(t2(...args))
}
| | 2 | ```typescript (…args:any)=> { t1(t2(…args)) }
| t3 | ```typescript
(...args)=>{
return ((...args:any)=> {
t1(t2(...args))
})(t3(...args))
}
|
最终得到的结果
const fn = (...args)=>{
return ((...args:any)=> {
t1(t2(...args))
})(t3(...args))
}
每次执行reduce
后,得到的函数,就将参数的执行包裹了一层。这样就能保证,每次a
的值都是一个函数。而且a
这个函数接受的参数就是b
执行的返回结果。
这样我们就能得到这样的效果:
- 函数从数组右边向左边执行。
- 上次一次执行的结果作为下一次的参数进行传递。
- 这样我们就能将一类函数关联起来。他们接受的参数的类型一样,返回类型一样。但是返回之后的函数执行之后,得到的函数的实现不一样。最后得到的函数执行,就是最开始那个函数的返回值。
将数组类型变成联合类型
```typescript type a = ({ a: number; b: string } | { a: string; b: number })[]
type b = a[number] // type b = { // a: number; // b: string; // } | { // a: string; // b: number; // } type c = b[‘a’] // number | string
<a name="FExNm"></a>
# 将联合类型变成交叉类型
```typescript
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer R) => void ? R : never
如果U是一个联合类型,那么就可以像这样变成交叉类型。
- 第一部分:
U extends any ? (k: U) => void : ...
- 这一部分如果
U
传入的是一个联合类型的话,那么我们得到的结果肯定是(k:U1) => void| (k:U2) => void
- 这一部分如果
第二部分:
一个函数 extends (k: infer R) => void ? R : never
这是将子接口继承父接口中的所有属性变成可选的。也是将T类型中的某一些属性变成可选的。
export type PartialProp<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
-
泛型的推断
// 如果没有传递泛型的话,P的类型就是NanoIdProps
function TT<P extends NanoIdProps>(): P {
// P类型可以是除了包含nanoid,a的所有的对象。这里就写死了,所以,报错了。必须写as P
const data = {
nanoid: '132',
a: 1
} as P
return data
}
如果一个泛型没有传递的且有extends的话,那么这个反省就是这个extends,
- 如果要给一个extends的泛型赋值,那么肯定会报错,因为,这个泛型的范围肯定比自己写的值的范围大。就相当于一部分类型就会丢掉了。就会出错。这个时候只能写
as
。定义一个加属性的hoc的类型
返回的组件的类型中要注入的类型。type HOC<InjectProps> = <props extends InjectProps>(
Component: ComponentType<props>
) => ComponentType<PartialProps<props, InjectProps>>
我们就能简单封装一下一个组件
interface NanoIdProps {
nanoid: string
a: number
}
type HOC<InjectProps> = <props extends InjectProps>(
Component: ComponentType<props>
) => ComponentType<PartialProps<props, InjectProps>>
/**
* 将传入的组件加入一个 nanoid 属性,使用这个组件的时候,就可以在Component的props中直接拿到nanoid属性
*
* @param Component 传入的组件
* @returns 返回一个新的组件
*/
export const withNanoIdHoc: HOC<NanoIdProps> = Component => {
// 返回的组件,属性只能传递P类型的属性
return props => {
const { nanoid: propNanoId, ...restProps } = props
const [nanoid, setNanoid] = useState(propNanoId)
useEffect(() => {
if (!nanoid) {
setNanoid(getNanoid())
}
}, [nanoid])
if (!nanoid) {
return null
}
const componentsProps = {
...restProps,
nanoid
} as ComponentProps<typeof Component> // 注意这里。类型必须要一样。
// eslint-disable-next-line react/jsx-props-no-spreading
return <Component {...componentsProps} />
}
}
这样就能实现单个的HOC增加属性。
但是如果有个多个HOC,每个HOC都要增加属性,比如第一个HOC增加高,第二个HOC增加宽,第三个HOC增加颜色。那么我们就需要层层嵌套。看起来很丑。我们一下子就想到了函数式编程中的compose
。
function compose<T1, T2>(hoc1: HOC<T1>, hoc2: HOC<T2>): HOC<T1 & T2>
function compose<T1, T2, T3>(
hoc1: HOC<T1>,
hoc2: HOC<T2>,
hoc3: HOC<T3>
): HOC<T1 & T2 & T3>
function compose<T1, T2, T3, T4>(
hoc1: HOC<T1>,
hoc2: HOC<T2>,
hoc3: HOC<T3>,
hoc4: HOC<T4>
): HOC<T1 & T2 & T3 & T4>
function compose<R>(hoc1: HOC<R>, ...hocList: HOC<any>[]): HOC<R>
function compose<R>(...funcs: HOC<any>[]): HOC<R>
// 具体的实现
function compose(...hocList: Array<HOC<any>>) {
return (c: ComponentType) => hocList.reduce((acc, hoc) => hoc(acc), c)
}
这样我们再次调用多个hoc就可以这样写了。执行顺序就是从前往后执行。前一个结果交给作为后一个hoc的参数。这样的话,就能无限的叠加属性了。
const Re = compose(
withNanoIdHoc,
withNanoIdHoc,
withNanoIdHoc,
withNanoIdHoc,
withNanoIdHoc
)(props => {
return <h1>{props.nanoid}</h1>
})
但是这样有个坏处就是:被增强的Component一开始初始化的属性不太好定义。需要手动的写并且加上之前所有增强的属性。
就像下面这样:如果外界要传递一些属性的话,必须在初始化的时候就定义好(这里就是传递给Test)。这样,才能在组件中能读到。
const Test: FC<NanoIdProps & { age: number }> = props => {
const { age } = props
return <h1>我是test,{age}</h1>
}
const Re = compose(
withNanoIdHoc,
withNanoIdHoc,
withNanoIdHoc,
withNanoIdHoc,
withNanoIdHoc
)(Test)
我们什么时候,直接进行穿参数,什么时候进行HOC呢?如果是一些公共的东西或者非常基础的东西。比如,增加权限管理或者其他的功能,其实都可以写在hoc中。
下面是OPP的操作方式
class Lifter<InjectProps> {
public static lift = <T extends object>(hoc: HOC<T>): Lifter<T> =>
new Lifter([hoc])
// eslint-disable-next-line no-useless-constructor
private constructor(private hocList: HOC<any>[]) {
//
}
public lift = <T extends object>(hoc: HOC<T>): Lifter<InjectProps & T> =>
new Lifter([...this.hocList, hoc])
public use: HOC<InjectProps> = Component =>
this.hocList.reduce((acc: ComponentType<any>, hoc) => hoc(acc), Component)
}
const Result = Lifter.lift(withNanoIdHoc).lift(withNanoIdHoc).use(Test)
我个人推荐都可以。看哪一种好理解就用哪一种。
还有一种使用的是使用Context
通过Context提供的Provider来提供数据。然后也能实现函数式的功能和属性增量。