☰
type-challenges 实战|Simple Vue:TypeScript this 推断 Hard 题四步拆解
2026/10/11 1:26:10 网站建设 项目流程

type-challenges 实战|Simple Vue:TypeScript this 推断 Hard 题四步拆解

【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址: https://gitcode.com/GitHub_Trending/ty/type-challenges

写过 Vue 的人都熟这一幕:组件里this.fullname直接返回拼好的字符串,不用写成this.computed.fullname()。编辑器补全里 data、computed、methods 的成员摊开在同一个this上,但各自"露"出来的形态并不一样。这背后是 Vue 声明文件里一段相当精巧的 this 推断。type-challenges 的 00006 Simple Vue(Hard)把这套机制单独拆出来考你:只给你一个带 data、computed、methods 的配置对象,让你给SimpleVue写出函数签名,让 TypeScript 在 computed 和 methods 的函数体内部把this推断正确。template.ts 的起点只有一行:

declare function SimpleVue(options: any): any

any进any出,推断全部失效。下面把它拆透。

题目到底在考什么

题目给的三条规则,本质是一张"暴露形态"对照表:

字段定义形式在 this 上的暴露形态能访问到什么
data返回对象的函数其返回值的各个字段什么都不行——连自己返回的字段、computed、methods 都不可见
computed函数对象去函数化后的返回值data 字段 + 计算属性值
methods函数对象原样保留为函数data 字段 + 计算属性值 + 其余方法

注意最后一行的"访问"范围:测试里hi()直接读了this.amount,所以 methods 的可见范围是 data、computed、methods 三者全量。心智模型一句话:三个字段共享同一个this,但 data 把自己关在门外,computed 露的是"值",methods 露的是"函数"。

测试用例就是规格书

官方 test-cases.ts 不用逐行读,按断言类别归成三组即可:

  1. data 内的三处@ts-expect-error必须真的报错——this.firstname(字段此时还不存在)、this.getRandom()(methods 不可见)、this.data()(自己不可见)。注意@ts-expect-error的工作机制:它声明"下一行必有错",如果你的签名放行得太多,这行没报错,这个指令本身反而会变成编译错误。
  2. computed / methods 内this可见性完整——this.firstname、this.amount、this.fullname、this.getRandom()全部畅通。
  3. this.fullname的精确类型必须是string,不是() => string。

第三种断言靠Expect<Equal<typeof fullname, string>>实现,这是仓库工具包里的一组类型级单测,写法长这样:

// Equal<A, B> 在 A 与 B 结构上"严格相等"时为 true,Expect 要求其为 true const cases: [Expect<Equal<typeof fullname, string>>] = [] as any

Equal比extends双向包含更苛刻,能抓住"函数 vs 值"这种暴露形态差异——这正是本题的分界线。

this 推断的解法推导:四步

第 1 步:把三个字段拆成独立泛型。

declare function SimpleVue<D, C, M>(options: { data: () => D computed: C methods: M }): any

TypeScript 会从实参对象把D推成 data 的返回类型、C/M推成两个对象字面量类型。拆出独立参数是后面做上下文运算的前提:this的类型最终都要由D、C、M拼出来,而它们只有被单独命名了才可引用。

第 2 步:用this: unknown锁死 data。

data: (this: unknown) => D

函数类型的this参数直接声明该函数体内this的类型。选unknown是因为它在属性访问上一律拒绝:this.firstname、this.getRandom()、this.data()全部编译失败——data 内三条@ts-expect-error就此满足。这一句就是"data 无法访问其他成员"的类型翻译。

第 3 步:用 ThisType 给 computed、methods 注入上下文。

ThisType<T>是 TS 内置标记类型:把它交叉进对象类型,整个对象字面量里每个方法的this都改由T推断。computed 看见"data 字段 + 计算属性值",methods 再多看见"全部方法":

computed: C & ThisType<D & Ctx> // Ctx 先留占位 methods: M & ThisType<D & Ctx & M>

用交叉而不是直接替换,是为了保留C/M原有的成员结构,只往this推断上追加信息。

第 4 步:条件类型 + infer 把 computed 去函数化。

Ctx不能直接用C——那样this.fullname是个函数,第 3 组断言全挂。要的是一个映射类型,把每个函数成员换成它的返回值:

type Ctx<C> = { [K in keyof C]: C[K] extends (...args: any[]) => infer R ? R : never }

infer R捕获函数签名里的返回值;never给非函数成员兜底。为什么不直接用ReturnType?它能跑,但条件类型版对非函数分支的处理更可控,本文就沿用前者。

组装成最终签名:

declare function SimpleVue<D, C, M>(options: { // data 封闭:this 上什么都没有 data: (this: unknown) => D // computed 内 this = data 字段 + 计算属性值(已去函数化) computed: C & ThisType<D & { [K in keyof C]: C[K] extends (...args: any[]) => infer R ? R : never }> // methods 内 this 再多一层 M:所有方法互相可见 methods: M & ThisType<D & { [K in keyof C]: C[K] extends (...args: any[]) => infer R ? R : never } & M> }): any

逐条回验测试用例:data 内this.firstname因this是unknown报错,三处@ts-expect-error成立;computed 的this.firstname类型来自D,是string;methods 的hi()里this.amount(number)、this.fullname(string)、this.getRandom()(() => number)分别落在D、映射类型、M上;test()里typeof this.fullname被Equal精确判定为string。全绿。

两个最容易翻车的点

先看一个反直觉的点:data 为什么是this: unknown而不是this: any?直觉上any是"万能通行证",可这里它恰恰是错的。this: any会静默放行一切属性访问,this.firstname不但能访问还不报错,@ts-expect-error落空、指令本身变成编译错误,测试直接红。unknown方向相反——它是最宽的类型却禁止任何属性访问,正好把 data 关进黑盒。一个"最宽松",一个"最严格",差别全在属性访问上。

第二个坑是生效范围。函数的this参数只锁住那一个函数自身——data里this是unknown,只说明 data 函数体内如此,对 computed、methods 毫无影响。ThisType<T>则作用于整个对象字面量:交叉进C之后,computed 里每一个方法(哪怕你写成独立函数再挂上去)的this都改从T推断。本题正是两者配合——前者把 data 单独封死,后者给另外两个字段统一供上下文。

顺带一提:第 4 步的条件类型写成C[K] extends ... ? ... : never,索引访问C[K]让分发发生在具体的K上,即使C是联合类型也能逐个成员处理,不会整体塌成never。

再走一步:Vue Basic Props

仓库里 6 号题的 info.yml 声明了关联题 213(Vue Basic Props)。它在同一个配置对象上加了props字段,考察的是构造器映射:props: { foo: Boolean }推断出{ foo: boolean };{ type: [Boolean, Number, String] }这类多构造函数形式得到联合类型boolean | number | string;空对象{}则退化为any;且 props 字段在 data、computed、methods 三处this上都可见。训练链很清楚:先吃透 6 号的 this 上下文注入,再叠加 213 的 props 类型提取,就逼近真实defineComponent的声明思路。

这套"泛型拆分 + ThisType 注入 + 映射类型变形"的组合拳,可以直接迁移到 Options API 组件库的声明文件、Pinia 里 getters 与 actions 的互相引用、以及任何配置驱动中间件的上下文设计上。TypeScript 版本会轻微影响 this 推断的细节,实际项目以锁定的版本为准。

【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址: https://gitcode.com/GitHub_Trending/ty/type-challenges

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询