TypeScript 类型体操之 - SimpleVue

对于 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的返回值类型可以是任意的。

ts 复制代码
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

所以我们的函数长这样:

ts 复制代码
declare function SimpleVue(options: {
  data: void;
  computed: void;
  methods: void;
}): any;

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

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

data

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

来逐句分析一下:

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

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

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

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

ts 复制代码
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 的类型为:

ts 复制代码
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 的类型为:

ts 复制代码
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 的时候已经解释过了,直接写:

ts 复制代码
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 能访问所有字段。

ts 复制代码
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

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

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

最终实现

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

ts 复制代码
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 类型有所帮助!

相关推荐
我要洋人死38 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#