TypeScript类型体操挑战(二十四)

47 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

困难

简单的 Vue 类型

挑战要求
在线示例

/** 获取 computed 中的 this */
type ComputedData<T> = {
  [P in keyof T]: T[P] extends () => infer R ? R : T[P]
}

type Option<D, C, M, Data = D extends () => infer R ? R : never> = {
  data: D,
  computed: C & ThisType<Data>,
  methods: M & ThisType<M & Data & ComputedData<C>>,
} & ThisType<never>

declare function SimpleVue<D, C, M>(options: Option<D, C, M>): any


// 使用示例
SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname
    // @ts-expect-error
    this.getRandom()
    // @ts-expect-error
    this.data()

    return {
      firstname: 'Type',
      lastname: 'Challenges',
      amount: 10,
    }
  },
  computed: {
    fullname() {
      return `${this.firstname} ${this.lastname}`
    },
  },
  methods: {
    getRandom() {
      return Math.random()
    },
    hi() {
      alert(this.amount)
      alert(this.fullname.toLowerCase())
      alert(this.getRandom())
    },
    test() {
      const fullname = this.fullname
      const cases: [Expect<Equal<typeof fullname, string>>] = [] as any
    },
  },
})

为了设置不同区域的 this 对象的类型,需要用到内置工具类型ThisType,相关介绍也可以看看我的笔记

首先了解一下ThisType的用法,下面定义了一段代码:

在线示例

interface Data {
  x: number,
  y: number
}

type Obj = {
  data: Data
  add: () => number
}

const obj: Obj = {
  data: {
    x: 1,
    y: 2,
  },
  add() {
    // 这里 this 就是 obj 对象,类型则对应 Obj
    return this.data.x + this.data.y;
  }
}

但是我想改变一下这个this对象的类型,那么我可以这么做:

// 使用内置工具类型 ThisType 声明上下文 this  的类型
type Obj = {
  data: Data
  add: () => number
} & ThisType<Data>

const obj: Obj = {
  data: {
    x: 1,
    y: 2,
  },
  add() {
    // 此时 this 的类型已经是 Data 了
    return this.x + this.y;
  }
}

需要注意的是,使用 ThisType 需要开启配置 noImplicitThis

弄明白 ThisType 后,再回到上面的答案中,就能明白是怎么回事了:

/*
  data 是函数,computed 和 methods 都是对象。
  所以,针对不同范围的上下文,需要单独处理。
  
  data 的上下文对象是 options
  computed 中函数的上下文对象是 options.computed
  methods 中函数的上下文对象是 options.methods
  所以使用 ThisType 给对应的对象进行处理即可
*/
type Option<D, C, M, Data = D extends () => infer R ? R : never> = {
  data: D,
  computed: C & ThisType<Data>,
  methods: M & ThisType<M & Data & ComputedData<C>>,
} & ThisType<never>

/*
  定义泛型 D、C、M 用来获取 data、
  computed、methods 的类型
*/
declare function SimpleVue<D, C, M>(options: Option<D, C, M>): any

柯里化 1

挑战要求
在线示例

// 组装函数
type Medium<T, R> =
  T extends [infer F, ...infer E]
    ? (arg: F) => Medium<E, R>
  : R;

declare function Currying<T>(fn: T):
  T extends (...args: infer Args) => infer R 
    ? Medium<Args, R> 
  : never


// 使用示例:
const add = (a: number, b: number) => a + b
const three = add(1, 2)

const curriedAdd = Currying(add)
const five = curriedAdd(2)(3)
  • 条件类型中使用infer获取函数的参数和返回值
  • 然后遍历数组Args,使用递归的方式组装函数即可

一开始我是这么来获取函数返回值的类型的:
declare function Currying<T extends any[], R>(fn: (...args: T) => R): Medium<T, R>

/*
  得到的结果如下,最后的返回值不够准确,不是 true
  const curried1: (arg: string) => boolean
*/
const curried1 = Currying((a: string) => true)


// 后面我换了种方式再获取函数的返回值
declare function Currying<T extends (...args: any[]) => unknown>(fn: T): ReturnType<T>
// curried1 的类型还是 boolean..
const curried1 = Currying((a: string) => true)

后来是去到解答区看到了这个解答,才知道要把这个类型推导放到最终结果处去进行处理:

declare function Currying<T>(fn: T): 
  T extends (...args: any[]) => infer R ? R : never

// 这次 curried1 的类型是 true 了,获得了具体的字面量类型
const curried1 = Currying((a: string) => true)

看了解答区的说明后,认为问题是出在函数的定义上:

// 3种读取函数的返回值的测试代码:
declare function f1(a: string): true  // 函数声明
declare var f2: (a: string) => true  // 匿名函数表达式

// 第一种,通过泛型在参数中定义。
declare function test1<R>(fn: (...args: any[]) => R): R;
test1(f1) // true ✅
test1(f2) // true ✅
test1((a: string) => true) // boolean ❌

// 第二种,通过泛型约束上去定义。
declare function test2<F extends (...args: any[]) => any>(fn: F): ReturnType<F>;
test2(f1) // true ✅
test2(f2) // true ✅
test2((a: string) => true) // boolean ❌

// 第三种,在泛型最终应用结果处再做判定。可以看出只有这种方法的效果最好,也是符合测试case的要求
declare function test3<F>(fn: F): F extends (...args: any[]) => any ? ReturnType<F> : never;
test3(f1) // true ✅
test3(f2) // true ✅
test3((a: string) => true) // true ✅