表示一个函数类型

  1. type Func = (...args: any[]) => any
  1. type Func<T extends any[], R> = (...a: T) => R
  2. // T 是参数,R是函数返回值

一个练习的例子

写出一个函数的重载:

  1. 如果没有参数的话,返回一个新的函数,新的函数的参数就是作为返回值

    1. export default function compose(): <R>(a: R) => R
  2. 如果有参数的话,只有一个参数的情况下,该参数必须是一个函数,且返回值也是该函数。(参数函数的特点就是:可以传递一个或者多个参数)

    1. export default function compose<F extends Func<unknown[], unknown>>(f: F): F
  3. 如果有两个或者多个参数的话,参数们都是函数,且返回值也是函数。返回函数的可以接受一个或者多个参数。且最后一个参数也接受一个或者多个参数。且后面函数的返回值作为前面函数的参数。(compose(f, g, h))=》(...args) => f(g(h(...args)))

    1. export default function compose<A, T extends unknown[], R>(f1: (a: A) => R, f2: Func<T, A>): Func<T, R>
    1. 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>

    同理,四个参数的情况,五个参数的情况就不列举了。

  4. 不知道传递几个参数的情况的函数重载。

    1. export default function compose<R>(
    2. f1: (a: unknown) => R,
    3. ...funcs: Func<unknown[], unknown>[]
    4. ): Func<unknown[], R>
    1. export default function compose<R>(
    2. ...func: Func<unknown[], unknown>[]
    3. ): Func<unknown[], R>

    实现这个函数重载的例子

    第一版

    1. export default function compose(...func: Func<unknown[], unknown>[]) {
    2. if (func.length === 0) {
    3. return (...args: unknown[]) => args
    4. }
    5. if (func.length === 1) {
    6. return func[0]
    7. }
    8. return function (...args: unknown[]) {
    9. let result: unknown
    10. for (let i = func.length - 1; i >= 0; i--) {
    11. const fn = func[i]
    12. if (i === func.length - 1) {
    13. result = fn(...args)
    14. } else {
    15. result = fn(result)
    16. }
    17. }
    18. return result
    19. }
    20. }

    从第一版实现我们就可以知道:
    如果是最后一个函数,可以接受多个参数,中间的函数参数就是以后面一个函数的的返回值来确定的。于是我们相当于就是要实现这样一个函数:

    1. compose(f, g, h) => (...args) => f(g(h(...args)))

    这种前后的值有关联的操作。我们就能想到用reduce给实现以下。

    第二版

    1. export default function compose(...func: Func<unknown[], unknown>[]) {
    2. if (func.length === 0) {
    3. return (...args: unknown[]) => args
    4. }
    5. if (func.length === 1) {
    6. return func[0]
    7. }
    8. return func.reduce((a, b) => {
    9. return (...args: unknown[]) => {
    10. return a(b(...args))
    11. }
    12. })
    13. // return func.reduce(
    14. // (a, b) =>
    15. // (...args: unknown[]) =>
    16. // a(b(...args))
    17. // )
    18. }

    我们来好好理解一下这个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. 1. 由于`reduce`每一步都是同一个之前函数,所以,最终要求返回一个函数。所以,reducer的回调也肯定是一个函数。这个没问题。
  2. 1. 最主要就是后面的函数的返回值作为下一个函数的参数。
  3. 1. 每次执行的ab的值以及reduce的返回值。
  4. | 第几次执行 | a | b | 返回值 |
  5. | --- | --- | --- | --- |
  6. | 1 | t1 | t2 | ```typescript
  7. (...args:any)=> {
  8. t1(t2(...args))
  9. }

| | 2 | ```typescript (…args:any)=> { t1(t2(…args)) }

  1. | t3 | ```typescript
  2. (...args)=>{
  3. return ((...args:any)=> {
  4. t1(t2(...args))
  5. })(t3(...args))
  6. }

|

最终得到的结果

  1. const fn = (...args)=>{
  2. return ((...args:any)=> {
  3. t1(t2(...args))
  4. })(t3(...args))
  5. }

每次执行reduce后,得到的函数,就将参数的执行包裹了一层。这样就能保证,每次a的值都是一个函数。而且a这个函数接受的参数就是b执行的返回结果。

这样我们就能得到这样的效果:

  1. 函数从数组右边向左边执行。
  2. 上次一次执行的结果作为下一次的参数进行传递。
  3. 这样我们就能将一类函数关联起来。他们接受的参数的类型一样,返回类型一样。但是返回之后的函数执行之后,得到的函数的实现不一样。最后得到的函数执行,就是最开始那个函数的返回值。

    将数组类型变成联合类型

    ```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

  1. <a name="FExNm"></a>
  2. # 将联合类型变成交叉类型
  3. ```typescript
  4. 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

    • 因为函数参数是逆变的,我们假设有一个变量能同时传给(k: 1) => void 和 (k: 2) => void,那么这个变量的类型应该是满足两个函数参数的类型,所以,传递参数满足参数1并且满足参数2都可
    • 所以满足上面的约束条件的R类型就是U1&U2

      将一个对象里面的某些属性变成可选的

      1. export type PartialProps<T extends K, K> = Partial<T> & Omit<T, keyof K>
  • 这是将子接口继承父接口中的所有属性变成可选的。也是将T类型中的某一些属性变成可选的。

    1. export type PartialProp<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
  • 这是将T类型中的某一些属性变成可以选的。

    泛型的推断

    1. // 如果没有传递泛型的话,P的类型就是NanoIdProps
    2. function TT<P extends NanoIdProps>(): P {
    3. // P类型可以是除了包含nanoid,a的所有的对象。这里就写死了,所以,报错了。必须写as P
    4. const data = {
    5. nanoid: '132',
    6. a: 1
    7. } as P
    8. return data
    9. }
  • 如果一个泛型没有传递的且有extends的话,那么这个反省就是这个extends,

  • 如果要给一个extends的泛型赋值,那么肯定会报错,因为,这个泛型的范围肯定比自己写的值的范围大。就相当于一部分类型就会丢掉了。就会出错。这个时候只能写as

    定义一个加属性的hoc的类型

    1. type HOC<InjectProps> = <props extends InjectProps>(
    2. Component: ComponentType<props>
    3. ) => ComponentType<PartialProps<props, InjectProps>>
    返回的组件的类型中要注入的类型。

我们就能简单封装一下一个组件

  1. interface NanoIdProps {
  2. nanoid: string
  3. a: number
  4. }
  5. type HOC<InjectProps> = <props extends InjectProps>(
  6. Component: ComponentType<props>
  7. ) => ComponentType<PartialProps<props, InjectProps>>
  8. /**
  9. * 将传入的组件加入一个 nanoid 属性,使用这个组件的时候,就可以在Component的props中直接拿到nanoid属性
  10. *
  11. * @param Component 传入的组件
  12. * @returns 返回一个新的组件
  13. */
  14. export const withNanoIdHoc: HOC<NanoIdProps> = Component => {
  15. // 返回的组件,属性只能传递P类型的属性
  16. return props => {
  17. const { nanoid: propNanoId, ...restProps } = props
  18. const [nanoid, setNanoid] = useState(propNanoId)
  19. useEffect(() => {
  20. if (!nanoid) {
  21. setNanoid(getNanoid())
  22. }
  23. }, [nanoid])
  24. if (!nanoid) {
  25. return null
  26. }
  27. const componentsProps = {
  28. ...restProps,
  29. nanoid
  30. } as ComponentProps<typeof Component> // 注意这里。类型必须要一样。
  31. // eslint-disable-next-line react/jsx-props-no-spreading
  32. return <Component {...componentsProps} />
  33. }
  34. }

这样就能实现单个的HOC增加属性。
但是如果有个多个HOC,每个HOC都要增加属性,比如第一个HOC增加高,第二个HOC增加宽,第三个HOC增加颜色。那么我们就需要层层嵌套。看起来很丑。我们一下子就想到了函数式编程中的compose

  1. function compose<T1, T2>(hoc1: HOC<T1>, hoc2: HOC<T2>): HOC<T1 & T2>
  2. function compose<T1, T2, T3>(
  3. hoc1: HOC<T1>,
  4. hoc2: HOC<T2>,
  5. hoc3: HOC<T3>
  6. ): HOC<T1 & T2 & T3>
  7. function compose<T1, T2, T3, T4>(
  8. hoc1: HOC<T1>,
  9. hoc2: HOC<T2>,
  10. hoc3: HOC<T3>,
  11. hoc4: HOC<T4>
  12. ): HOC<T1 & T2 & T3 & T4>
  13. function compose<R>(hoc1: HOC<R>, ...hocList: HOC<any>[]): HOC<R>
  14. function compose<R>(...funcs: HOC<any>[]): HOC<R>
  15. // 具体的实现
  16. function compose(...hocList: Array<HOC<any>>) {
  17. return (c: ComponentType) => hocList.reduce((acc, hoc) => hoc(acc), c)
  18. }

这样我们再次调用多个hoc就可以这样写了。执行顺序就是从前往后执行。前一个结果交给作为后一个hoc的参数。这样的话,就能无限的叠加属性了。

  1. const Re = compose(
  2. withNanoIdHoc,
  3. withNanoIdHoc,
  4. withNanoIdHoc,
  5. withNanoIdHoc,
  6. withNanoIdHoc
  7. )(props => {
  8. return <h1>{props.nanoid}</h1>
  9. })

但是这样有个坏处就是:被增强的Component一开始初始化的属性不太好定义。需要手动的写并且加上之前所有增强的属性。
就像下面这样:如果外界要传递一些属性的话,必须在初始化的时候就定义好(这里就是传递给Test)。这样,才能在组件中能读到。

  1. const Test: FC<NanoIdProps & { age: number }> = props => {
  2. const { age } = props
  3. return <h1>我是test,{age}</h1>
  4. }
  5. const Re = compose(
  6. withNanoIdHoc,
  7. withNanoIdHoc,
  8. withNanoIdHoc,
  9. withNanoIdHoc,
  10. withNanoIdHoc
  11. )(Test)

我们什么时候,直接进行穿参数,什么时候进行HOC呢?如果是一些公共的东西或者非常基础的东西。比如,增加权限管理或者其他的功能,其实都可以写在hoc中。

下面是OPP的操作方式

  1. class Lifter<InjectProps> {
  2. public static lift = <T extends object>(hoc: HOC<T>): Lifter<T> =>
  3. new Lifter([hoc])
  4. // eslint-disable-next-line no-useless-constructor
  5. private constructor(private hocList: HOC<any>[]) {
  6. //
  7. }
  8. public lift = <T extends object>(hoc: HOC<T>): Lifter<InjectProps & T> =>
  9. new Lifter([...this.hocList, hoc])
  10. public use: HOC<InjectProps> = Component =>
  11. this.hocList.reduce((acc: ComponentType<any>, hoc) => hoc(acc), Component)
  12. }
  13. const Result = Lifter.lift(withNanoIdHoc).lift(withNanoIdHoc).use(Test)

我个人推荐都可以。看哪一种好理解就用哪一种。

还有一种使用的是使用Context

通过Context提供的Provider来提供数据。然后也能实现函数式的功能和属性增量。