likes
comments
collection
share

TypeScript 类型体操之 - SimpleVue

作者站长头像
站长
· 阅读数 4

对于 TypeScript 类型系统感兴趣的同学,估计都听过 type-challenges 项目,俗称类型体操。

本文详细记录了第一道 hard 难度的题目 SimpleVue 的解题过程。

前排提示:答案并不唯一,官网的 issue 区有很多优秀的答案,但很多都没有解题过程,需要大家自行理解。

题目描述

实现类似 Vue 的类型支持的简化版本。

通过提供一个函数SimpleVue(类似于Vue.extenddefineComponent),它应该正确地推断出 computed 和 methods 内部的this类型。

在此挑战中,我们假设SimpleVue接受只带有datacomputedmethods字段的 Object 作为其唯一的参数,

  • data是一个简单的函数,它返回一个提供上下文this的对象,但是你无法在data中获取其他的计算属性或方法。

  • computed是将this作为上下文的函数的对象,进行一些计算并返回结果。在上下文中应暴露计算出的值而不是函数。

  • methods是函数的对象,其上下文也为this。函数中可以访问datacomputed以及其他methods中的暴露的字段。 computedmethods的不同之处在于methods在上下文中按原样暴露为函数。

SimpleVue的返回值类型可以是任意的。

const instance = SimpleVue({
  data() {
    return {
      firstname: 'Type',
      lastname: 'Challenges',
      amount: 10,
    };
  },
  computed: {
    fullname() {
      return this.firstname + ' ' + this.lastname;
    },
  },
  methods: {
    hi() {
      alert(this.fullname.toLowerCase());
    },
  },
});

解题过程

根据题目描述,函数 SimpleVue 有三个部分:data, computedmethods

所以我们的函数长这样:

declare function SimpleVue(options: {
  data: void;
  computed: void;
  methods: void;
}): any;

那么我们根据泛型来接受它的参数,再通过题目的意思一一返回。

declare function SimpleVue<TData, TComputed, TMethods>(options: {
  data: void;
  computed: void;
  methods: void;
}): any;

data

题目描述:data 是一个简单的函数,它返回一个提供上下文 this 的对象,但是你无法在 data 中获取其他的计算属性或方法。

来逐句分析一下:

  1. 一个简单的函数
data: () => any;
  1. 它返回一个提供上下文 this 的对象

也就是说 data 的会返回一个对象,我们把它给到 TData,之后在 computedmethods 中会用到。

data: () => TData;
  1. 但是你无法在 data 中获取其他的计算属性或方法

意思是函数内部不依赖于任何对象的 this 上下文,即不使用对象的属性或方法。测试用例中也可以看到,在 data 中任何 this.xxx 都应该报错。

data() {
  // @ts-expect-error
  this.firstname;
  // @ts-expect-error
  this.getRandom();
  // @ts-expect-error
  this.data();

  return {
    firstname: 'Type',
    lastname: 'Challenges',
    amount: 10,
  };
}

要做到这一点,可以使用 this: void 实现。以下是来自 ChatGPT 的回答:

在 TypeScript 中,this: void 是一种函数签名的写法,表示函数不期望在其执行期间引用任何特定的 this 上下文。它指定了函数在被调用时,this 的类型为 void,即不允许使用任何对象的上下文。

所以 data 的类型为:

declare function SimpleVue<TData, TComputed, TMethods>(options: {
  data: (this: void) => TData;
  computed: void;
  methods: void;
}): any;

computed

题目描述:computed 是将 this 作为上下文的函数的对象,进行一些计算并返回结果。在上下文中应暴露计算出的值而不是函数。

逐句分析一下:

  1. this 作为上下文的函数的对象

这里的 this 指的是 data 函数中返回的对象,也就是 TData

要将 TData 作为 computed 的上下文,需要用到 ThisType 类型。关于 ThisType 的定义,我在网上找到了一个容易理解的解释:

如果将 & ThisType<WhateverYouWantThisToBe> 添加到对象的类型,则该对象内的函数将使用 WhateverYouWantThisToBe 作为 this 的类型。

同样的,我们把 computed 的类型给到 TComputed,在之后的 methods 里会用到。

所以 computed 的类型为:

declare function SimpleVue<TData, TComputed, TMethods>(options: {
  data: (this: void) => TData;
  computed: TComputed & ThisType<TData>;
  methods: void;
}): any;
  1. 在上下文中应暴露计算出的值而不是函数。

这个是指在 methods 中,只能得到 computed 对象中的函数的返回值类型,我们会在 methods 中实现。

methods

最后是 methods 部分,也是本道题较为复杂的一部分。

题目描述:

methods 是函数的对象,其上下文也为 this。函数中可以访问 datacomputed 以及其他 methods 中的暴露的字段。 computedmethods 的不同之处在于 methods 在上下文中按原样暴露为函数。

还是老规矩,逐句解析:

  1. methods 是函数的对象,其上下文也为 this

computed 的时候已经解释过了,直接写:

declare function SimpleVue<TData, TComputed, TMethods>(options: {
  data: (this: void) => TData;
  computed: TComputed & ThisType<TData>;
  methods: TMethods & ThisType<TData>;
}): any;
  1. 函数中可以访问 datacomputed 以及其他 methods 中的暴露的字段。 computedmethods 的不同之处在于 methods 在上下文中按原样暴露为函数。

我们把最后两句放在一起解析,简单来说,methods 能访问所有字段。

declare function SimpleVue<TData, TComputed, TMethods>(options: {
  data: (this: void) => TData;
  computed: TComputed & ThisType<TData>;
  methods: TMethods & ThisType<TData & TComputed & TMethods>;
}): any;

这个时候会发现测试用例中有两处报错,都是因为 this.fullname 导致的,因为目前 this.fullname 是函数类型,而题目要求了 computed 在上下文中应暴露计算出的值而不是函数。

所以我们需要将 computed 对象中每个函数的返回值组成一个新的类型返回,我们叫它 GetComputed

type GetComputed<T> = {
  [P in keyof T]: T[P] extends (...args: any) => infer R ? R : never;
};

Computed 的实现就比较简单了,遍历 computed 中的 key,使用 extends 关键字看是不是函数类型,使用 infer 关键字得到返回值类型。

最终实现

所以这道题的最终实现为:

type GetComputed<T> = {
  [P in keyof T]: T[P] extends (...args: any) => infer R ? R : never;
};

declare function SimpleVue<TData, TComputed, TMethods>(options: {
  data: (this: void) => TData;
  computed: TComputed & ThisType<TData>;
  methods: TMethods & ThisType<TData & GetComputed<TComputed> & TMethods>;
}): any;

总结

至此这道题就算解决了,完结撒花~🌹🌹🌹

对于刚开始刷 TypeScript 类型体操的人,这道题还是有一定难度的,用到的知识点也比较多。

比如:

  • (this: void) => any
  • ThisType 的用法
  • extendsinfer 等关键字的用法

强烈建议想刷 TypeScript 类型体操的同学先学一下 TypeScript 内置的各种类型,以及常见的套路等。type-challengesREADME 中列出来的学习资源就很不错。

希望这道题对大家理解 TypeScript 类型有所帮助!