惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
GbyAI
GbyAI
博客园_首页
V
Visual Studio Blog
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 叶小钗
腾讯CDC
博客园 - Franky
IT之家
IT之家
Google DeepMind News
Google DeepMind News
Microsoft Azure Blog
Microsoft Azure Blog
D
Docker
大猫的无限游戏
大猫的无限游戏
Recent Announcements
Recent Announcements
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell

Anthony Fu

Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory (React ver.) Mental Health in Open Source The Evolution of Shiki v1.0 The Magic in Shiki Magic Move Anthony's Roads to Open Source - The Progressive Path Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory Now, and the Future of Nuxt Devtools Anthony's Roads to Open Source - The Set Theory Anthony Fu Anthony Fu Stable Diffusion QR Code 101 Refining AI Generated QR Code Stylistic QR Code with Stable Diffusion Anthony Fu How I Manage GitHub Notifications
Anthony Fu
Anthony Fu · 2020-06-28 · via Anthony Fu
  • Type for this
    • ThisType

As you may or may not know, I am working on preparing to release the v1.0 version for @vue/composition-api recently. One of the current problems is that the type inference does not play well #338. So I get a chance to have a deeper look at vue-next’s type implementations. I will tell you what I learned and how magic works in Vue.

Forget about the setup() function and Composition API for now, let talk about the options API in Vue 2 that everybody familiar with. In a classical example, we would have data, computed, methods and some other fields like this:

export default {
  data: {
    first_name: 'Anthony',
    last_name: 'Fu',
  },
  computed: {
    full_name() {
      return `${this.first_name} ${this.last_name}`
    },
  },
  methods: {
    hi() {
      alert(this.full_name)
    }
  }
}

It works well in JavaScript and putting all the context into this is pretty straightforward and easy to understand. But when you switch to TypeScript for static type checking. this will not be the context you expected. How can we make the types work for Vue like the example above?

Type for this

To explicitly assign the type to this, we can simply use the this parameter:

interface Context {
  $injected: string
}

function bar(this: Context, a: number) {
  this.$injected // ok
}

The limitation of this approach is that we will lose the signature of the method when working with a dict of methods:

type Methods = Record<string, (this: Context, ...args: any[]) => any>

const methods: Methods = {
  bar(a: number) {
    this.$injected // ok
  }
}

methods.bar('foo', 'bar') // no error, the type of arguments becomes `any[]`

We would not want to ask users to explicitly type this in every method in order to make the type checking works. So we will need another approach.

ThisType

After digging into Vue’s code, I found an interesting TypeScirpt utility ThisType. The official doc says:

This utility does not return a transformed type. Instead, it serves as a marker for a contextual this type.

ThisType would affect all the nested functions. With it, we can have:

interface Methods {
  double: (a: number) => number
  deep: {
    nested: {
      half: (a: number) => number
    }
  }
}

const methods: Methods & ThisType<Methods & Context> = {
  double(a: number) {
    this.$injected // ok
    return a * 2
  },
  deep: {
    nested: {
      half(a: number) {
        this.$injected // ok
        return a / 2
      }
    }
  }
}

methods.double(2) // ok
methods.double('foo') // error
methods.deep.nested.half(4) // ok

The typing works well, but it still requires users to define the type interface of Methods first. Can we make it infer itself automatically?

We can do that with function inference:

type Options<T> = {
  methods?: T
} & ThisType<T & Context>

function define<T>(options: Options<T>) {
  return options
}

define({
  methods: {
    foo() {
      this.$injected // ok
    },
  },
})

There is only one step left, to make context object dynamic inference from data and computed.

The full working demo would be:

/* ---- Type ---- */
export type ExtractComputedReturns<T extends any> = {
  [key in keyof T]: T[key] extends (...args: any[]) => infer TReturn
    ? TReturn
    : never
}

type Options<D = {}, C = {}, M = {}> = {
  data: () => D
  computed: C
  methods: M
  mounted: () => void
  // and other options
}
& ThisType<D & M & ExtractComputedReturns<C>> // merge them together

function define<D, C, M>(options: Options<D, C, M>) {}

/* ---- Usage ---- */
define({
  data() {
    return {
      first_name: 'Anthony',
      last_name: 'Fu',
    }
  },
  computed: {
    fullname() {
      return `${this.first_name} ${this.last_name}`
    },
  },
  methods: {
    notify(msg: string) {
      alert(msg)
    }
  },
  mounted() {
    this.notify(this.fullname)
  },
})