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): anyany进any出,推断全部失效。下面把它拆透。
题目到底在考什么
题目给的三条规则,本质是一张"暴露形态"对照表:
| 字段 | 定义形式 | 在 this 上的暴露形态 | 能访问到什么 |
|---|---|---|---|
| data | 返回对象的函数 | 其返回值的各个字段 | 什么都不行——连自己返回的字段、computed、methods 都不可见 |
| computed | 函数对象 | 去函数化后的返回值 | data 字段 + 计算属性值 |
| methods | 函数对象 | 原样保留为函数 | data 字段 + 计算属性值 + 其余方法 |
注意最后一行的"访问"范围:测试里hi()直接读了this.amount,所以 methods 的可见范围是 data、computed、methods 三者全量。心智模型一句话:三个字段共享同一个this,但 data 把自己关在门外,computed 露的是"值",methods 露的是"函数"。
测试用例就是规格书
官方 test-cases.ts 不用逐行读,按断言类别归成三组即可:
- data 内的三处
@ts-expect-error必须真的报错——this.firstname(字段此时还不存在)、this.getRandom()(methods 不可见)、this.data()(自己不可见)。注意@ts-expect-error的工作机制:它声明"下一行必有错",如果你的签名放行得太多,这行没报错,这个指令本身反而会变成编译错误。 - computed / methods 内
this可见性完整——this.firstname、this.amount、this.fullname、this.getRandom()全部畅通。 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 anyEqual比extends双向包含更苛刻,能抓住"函数 vs 值"这种暴露形态差异——这正是本题的分界线。
this 推断的解法推导:四步
第 1 步:把三个字段拆成独立泛型。
declare function SimpleVue<D, C, M>(options: { data: () => D computed: C methods: M }): anyTypeScript 会从实参对象把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),仅供参考