This is one of the most difficult features in Javascript to understand, and Typescript this is even more complex. There are three scenarios for this in Typescript, each of which has a different meaning.

  • This parameter: Limits the type of this when a function is called
  • This type: used to support chain calls, especially class inherited chain calls
  • ThisType: Used to construct complex factory functions

This parameter

Because javascript supports flexible function calls, the direction of this varies depending on the call scenario

  • Method invocation as an object
  • Called as a normal function
  • Called as a constructor
  • Call as function.prototype. call and function.prototype. bind

Object method call

This is also the case for most this, which refers to an object when a function is called as a method of that object

const obj = {
  name: "yj".getName() {
    return this.name {name:string, getName():string}
  },
}
obj.getName() / / type string
Copy the code

The catch here is that if the object method is defined using the arrow function, this does not refer to the object but to the global window, and Typescript automatically does that for me

const obj2 = {
  name: "yj".getName: () = > {
    return this.name // check indicates an error, where this refers to window
  },
}
obj2.getName() // Run an error
Copy the code

Ordinary function calls

Even functions defined through non-arrow functions, when assigned to a variable and called directly from the variable, their runtime this executes on something other than the object itself

const obj = {
  name: "yj".getName() {
    return this.name
  },
}
const fn1 = obj.getName
fn1() // This refers to window
Copy the code

Unfortunately, the above code was not detected at compile time, which we can fix by adding the this type annotation to getName

interface Obj {
  name: string
  // qualify the this type for the getName call
  getName(this: Obj): string
}
const obj: Obj = {
  name: "yj".getName() {
    return this.name
  },
}
obj.getName() // check ok
const fn1 = obj.getName
fn1() // check error
Copy the code

That way we can declare type-safe this when we call this

Constructor call

Before the advent of class, function was used as a constructor. When function was called through new, this in the constructor referred to the return object

function People(name: string) {
  this.name = name // check error
}
People.prototype.getName = function() {
  return this.name
}
const people = new People() // check error
Copy the code

Unfortunately, Typescript does not currently support type inference for ES5’s constructor function (github.com/microsoft/T… The type of this and people can be called as constructors, so the type annotation needs to be explicit

interface People {
  name: string
  getName(): string
}
interface PeopleConstructor {
  new (name: string): People // Declarations can be called as constructors
  prototype: People // Declare prototype. Support subsequent changes to prototype
}
const ctor = (function(this: People, name: string) {
  this.name = name
} as unknown) as PeopleConstructor // Type incompatible, secondary transformation

ctor.prototype.getName = function() {
  return this.name
}

const people = new ctor("yj")
console.log("people:", people)
console.log(people.getName())
Copy the code

The simplest way, of course, is to use class

class People {
  name: string
  constructor(name: string) {
    this.name = name // check ok
  }
  getName() {
    return this.name
  }
}

const people = new People("yj") // check ok
Copy the code

There is a fundamental difference between public field methods and methods in class. Consider the following three methods

class Test {
  name = 1
  method1() {
    return this.name
  }
  method2 = function() {
    return this.name // check error
  }
  method3 = () = > {
    return this.name
  }
}

const test = new Test()

console.log(test.method1()) / / 1
console.log(test.method2()) / / 1
console.log(test.method3()) / / 1
Copy the code

All three of the above code successfully output 1, but there are essential differences

  • Method1: Prototype methods, dynamic this, and you need to manually bind this for asynchronous callback scenarios
  • Method2: Instance method. Type error. You need to manually bind this in asynchronous scenarios
  • Method3: Instance methods, static this, no need to manually bind this in asynchronous scenarios

When writing the React application, we used method3 a lot to automatically bind this, but in practice this has major problems

  • Each instance creates an instance method, resulting in waste
  • When dealing with inheritance, this leads to a counterintuitive phenomenon
class Parent {
  constructor() {
    this.setup()
  }

  setup = () = > {
    console.log("parent")}}class Child extends Parent {
  constructor() {
    super()
  }

  setup = () = > {
    console.log("child")}}const child = new Child() // parent

class Parent2 {
  constructor() {
    this.setup()
  }

  setup() {
    console.log("parent")}}class Child2 extends Parent2 {
  constructor() {
    super()}setup() {
    console.log("child")}}const child2 = new Child2() // child
Copy the code

When dealing with inheritance, if the superclass calls a sample method instead of a prototype method, it cannot override in the subclass. This is contrary to the behavior of other languages dealing with inherited overrides, which is quite problematic. So it makes more sense not to use instance methods, but what about the binding of this? It makes sense to either bind manually or use a decorator to bind

import autobind from "autobind-decorator"
class Test {
  name = 1
  @autobind
  method1() {
    return this.name
  }
}
Copy the code

Call and apply calls

There are no essential differences between call and apply calls. The main difference is that arguments are passed around. In contrast to normal function calls, call calls can dynamically change the incoming this. Fortunately, Typescript also supports type checking for call calls with the this parameter

interface People {
  name: string
}
const obj1 = {
  name: "yj".getName(this: People) {
    return this.name
  },
}
const obj2 = {
  name: "zrj",}const obj3 = {
  name2: "zrj",
}
obj1.getName.call(obj2)
obj1.getName.call(obj3) // check error
Copy the code

The implementation of call is also very interesting. We can briefly explore its implementation. Our implementation is called call2, and we need to determine the type of the first parameter in call. We can use ThisParameterType to get the this parameter type of a function

interface People {
  name: string
}
function ctor(this: People) {}

type ThisArg = ThisParameterType<typeof ctor> // Is of type People
Copy the code

ThisParameterType is also easy to implement with infer Type

type ThisParameterType<T> = T extends (this: unknown, ... args:any[]) = >any
  T extends (this: infer U, ... args:any[]) = >any
  ? U
  : unknown
Copy the code

But how do we get the type of the current function, through generic instantiation and generic constraints

interface CallableFunction {
  call2<T>(this: (this: T) = > any.thisArg: T): any
}
interface People {
  name: string
}
function ctor(this: People) {}
ctor.call2() //
Copy the code

When ctor.call is made, according to the definition of CallableFunction, the type of this parameter is (this:T) => any, and this is cTOR. According to the definition of CTRo, If (this:People) => any, then thisArg is of type People

Further add the return value and the remaining parameter types

interface CallableFunction {
  call<T, A extends any[], R>(
    this: (this: T, ... args: A) = > R,
    thisArg: T, ... args: A ): R }Copy the code

This Types

In order to support the Fluent Interface, the return type of the supported method is determined by the invocation example, which actually requires additional support from the type system. Consider the following code

class A {
  A1() {
    return this
  }
  A2() {
    return this}}class B extends A {
  B1() {
    return this
  }
  B2() {
    return this}}const b = new B()
const a = new A()
b.A1().B1() / / is not an error
a.A1().B1() / / an error
type M1 = ReturnType<typeof b.A1> // B
type M2 = ReturnType<typeof a.A1> // A
Copy the code

A closer look at the code shows that, in different cases, the return type of A1 is actually specific to the calling object rather than fixed. This is the only way to support the following chain calls, ensuring that each step of the call is type-safe

b.A1()
  .B1()
  .A2()
  .B2() // check ok
Copy the code

There’s something special about this. Most languages treat this as an implicit argument, but for functions its arguments should be contravariant, but this is actually treated as a covariant. Consider the following code

class Parent {
  name: string
}
class Child extends Parent {
  age: number
}
class A {
  A1() {
    return this.A2(new Parent())
  }
  A2(arg: Parent) {}
  A3(arg: string){}}class B extends A {
  A1() {
    // This is treated as covariant
    return this.A2(new Parent())
  }
  A2(arg: Child) {} // Error reported in flow, typescript does not
  A3(arg: number) {} // Errors are reported in both flow and typescript
}
Copy the code

One other point to note here is that Typescript is compatible with its dual variant of methods, but still uses inverse functions, compared to flow, which is much safer and uses inverse methods

ThisType

One of the biggest criticisms of vue2.x is its weak support for Typescript. This is also due to the heavy use of this in the VUe2. x API, which makes it hard to infer its type. Vue2.5 has a wave of enhancements to vue’s typescript support with ThisType, but there are still some downsides. One of Vue3’s big selling points is improved and enhanced typescript support. Let’s take a look at how ThisType can be used to improve Typescript support in ThisType and Vue.

A brief description of the decision rule for This. We assume that the rule for This type of object method is as follows, from lower priority to higher priority

The this type of an object literal method is the object literal itself

// containing object literal type
let foo = {
  x: "hello".f(n: number) {
    this //this: {x: string; f(n: number):void }}},Copy the code

If the object literal is typed, this is the type of the annotated object

type Point = {
  x: number
  y: number
  moveBy(dx: number.dy: number) :void
}

let p: Point = {
  x: 10.y: 20.moveBy(dx, dy) {
    this // Point}},Copy the code

If the object literal method has a this annotation, it is the annotation’s this

let bar = {
  x: "hello".f(this: { message: string }) {
    this // { message: string }}},Copy the code

If the object literal is typed and the method is typed, the method this takes precedence

type Point = {
  x: number
  y: number
  moveBy(dx: number.dy: number) :void
}

let p: Point = {
  x: 10.y: 20.moveBy(this: { message: string }, dx, dy) {
    this // {message:string}, the method type annotation takes precedence over the object type annotation}},Copy the code

If the object literal is typed and the type annotation contains ThisType, then this isT

type Point = {
  x: number
  y: number
  moveBy: (dx: number, dy: number) = > void
} & ThisType<{ message: string} >let p: Point = {
  x: 10.y: 20.moveBy(dx, dy) {
    this // {message:string}}},Copy the code

If the object literal is typed and the type annotation specifies this, then that annotation type is used

type Point = {
  x: number
  y: number
  moveBy(this: { message: string }, dx: number.dy: number) :void
}

let p: Point = {
  x: 10.y: 20.moveBy(dx, dy) {
    this // { message:string}}},Copy the code

Arrange the rules as follows, from highest to lowest

  • If the method displays an annotation of this type, that annotation type is used
  • If this is not marked above, but is marked by a method type in the type of the object, then this is used
  • If none of the above is marked, but the object’s type contains ThisType, then this isT
  • If none of the above is called, this is the annotation type of the object
  • If none of the above is indicated, this is the object literal type

An important rule here is that in the absence of other type annotations, if the object’s annotation type contains ThisType, then this type isT. This means that we can use type evaluation to add attributes to our object literals that do not exist in the literals. This is extremely important for Vue. Let’s take a look at Vue’s API

import Vue from 'vue';
export const Component = Vue.extend({
  data(){
    return {
      msg: 'hello'}}methods: {greet(){
      return this.msg + 'world'; }}})Copy the code

The greet is the greet of methods (greet), and this is the literal type of methods (greet). Therefore, we cannot get the type of data from the middle section. Therefore, the main problem is how to access MSG in data safely from methods. Greet. It’s easy to implement with generic derivations and ThisType, so let’s implement some of this API ourselves

type ObjectDescriptor<D, M> = {
  data: () = > D
  methods: M & ThisType<D & M>
}

declare function extend<D.M> (obj: ObjectDescriptor<D, M>) :D & M

const x = extend({
  data() {
    return {
      msg: "hello",
    }
  },
  methods: {
    greet() {
      return this.msg + "world" // check
    },
  },
})
Copy the code

The derivation rules are as follows: first, the instantiation type results of type parameters T and M can be obtained by comparing the types and generic constraints of object literals

D: { msg: string}
M: {
  greet(): todo
}
Copy the code

And then the ObjectDescriptor is of type

{
  data(): { msg: string},
  methods: {
    greet(): string
  } & ThisType<{msg:string} & {greet(): todo}>
}
Copy the code

And then, with the help of the ObjectDescriptor, the this type in greet is deduced to be

{ msg: string} & { greet(): todo}
Copy the code

MSG is of type string and greet is of type string. In addition, in order to reduce Typescript type flipping, annotation types should be displayed as much as possible to prevent loop derivation or complexity that leads to slow compilations, endless loops, or memory depletion.

type ObjectDescriptor<D, M> = {
  data: () = > D
  methods: M & ThisType<D & M>
}

declare function extend<D.M> (obj: ObjectDescriptor<D, M>) :D & M

const x = extend({
  data() {
    return {
      msg: "hello",
    }
  },
  methods: {
    greet(): string {
      //Display the annotation return type, simplifying the derivationreturn this.msg + "world" // check
    },
  },
})
Copy the code