4


Contact us : Youdao technical team assistant : ydtech01 / Email : ydtech@rd.netease.com

This article is the last one in the "Fun TypeScript Tool Type" series, and contains the following parts:

  • ThisParameterType<Type>
  • OmitThisParameter<Type>
  • ThisType<Type>

Quick Jump

1. ThisParameterType<Type>

Extract a function type with the this parameter defined explicitly by . If there is no this parameter explicitly defined, then unknown will be returned. Here are the following points to note:

  • The this parameter can only be called this and must be in the first position of the parameter list
  • this must be explicitly defined
  • The this parameter does not exist when the function is actually called, and does not need to be explicitly passed in as a parameter, but is specified by methods such as call, apply, or bind.

1.1 Source code analysis

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

It can be seen from the source code that the type parameter T must strictly match the (this: infer U, ...args: any[]) => any , so the name and position of the this parameter are fixed. The rest of the logic is to define a type parameter U for the type of this parameter, and return this type parameter U when extends judges to go to the true branch, and return unknown for the false branch.

1.2 Practical usage

Explicitly defining this type helps us use this safe

function toHex(this: Number) {
  return this.toString(16);
}
 
function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

Note: A function is defined. To use the type of this function, you can use typeof [funcName] directly, and you can omit the need to define an additional type declaration.

2. OmitThisParameter<Type>

With ThisParameterType gets the type of this, then how to remove the this parameter type from a function type that defines the this parameter type? This is what OmitThisParameter does.

In one sentence, directly returns this function type for a function type that does not define the this parameter type. If the this parameter type is defined, it returns a new function type with the this parameter type removed.

2.1 Source code analysis

type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;

It seems a bit long. In fact, it is two nested extends conditional judgments. It is easy to understand if it is divided into two parts. First, is:

unknown extends ThisParameterType<T> ? T : ...

For the passed function type T, first use ThisParameterType to get the type of this parameter. There may be two results. One is that successfully gets the this parameter type and returns , and the other is unknown. So if unknown is returned, then the true branch is taken and T is returned directly. If it is not the unknown returned, then go to the false branch, namely:

T extends (...args: infer A) => infer R ? (...args: A) => R : T

It is another conditional judgment, that is, as long as T is a legal function type, it must satisfy (...args: infer A) => infer R , the rest is to define a type parameter A for the parameter, and for the return value Define a type parameter R and return (...args: A) => R . This new function type no longer contains this.

2.2 Practical usage

function toHex(this: Number) {
  return this.toString(16);
}
 
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
 
console.log(fiveToHex());

3. ThisType<Type>

This tool type is very special. The first special feature is its source code definition , which is a empty interface :

/**
 * Marker for contextual 'this' type
 */
interface ThisType<T> { }

So what is the role of ThisType? As the official comment writes: used as a mark of the this type of context.

To use ThisType, you must ensure that the noImplicitThis configuration is turned on, and we will only discuss the case of turning it on later.

So how to understand this sentence? We need to understand from the actual effect, first look at the following code:

let demo1 = {
  a: 'lipengpeng',
  test(msg: string) {
    this;
  }
};

What is its this type?

this: {
    a: string;
    test(msg: string): void;
}

You can also manually specify this type, for example:

let demo2 = {
  a: 'lipengpeng',
  test(this:{a: string}, msg: string) {
    this;
  }
};

The this type at this time is

this: {
    a: string
}

In fact, this is only ideal case of this type analysis , because TypeScript is a type inferred by static code analysis, this may change in the actual operation stage, so how do we specify the this type in the operation stage for .

If you only look at the above two situations, you may think that this type is not enough, because TypeScript will infer this type, but this is only a simple situation, as we mentioned before, this in the runtime phase can be changed, so it is only dependent Code analysis cannot predict the future of this type. At this time, we need to use our protagonist-ThisType. We continue to start from the actual usage scenarios. In actual development, we define an object and sometimes give a data structure, which is similar to Vue2.x Options API :

let options = {
  data: {
    x: 0,
    y: 0
  },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    }
  }
}

We hope that the x and y in the data object can be directly obtained on the this object of moveBy. In order to achieve this function, we need to do some processing on the defined data structure so that the attributes in methods and data share the same this object, so we need a tool method makeObject :

function makeObject(config) {
  let data = config?.data || {}
  let methods = config?.methods || {}
  return {
    ...data,
    ...methods
  }
}

let options = makeObject({
  data: {
    x: 0,
    y: 0
  },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    }
  }
})

The method is also very simple, that is, expand data and methods and place them in the same object options. When we call moveBy through options.moveBy(), the this of moveBy is this object. The function is implemented, so how to achieve type safety? Next, we need to make some changes in the makeObject method. The key point is to define the parameter types and return value types :

// 只考虑传入makeObject的config参数只包含data和methods两个参数
// 定义两个泛型参数D & M来代表它们的类型
type ObjectConfigDesc<D, M> = {
  data: D
  methods: M
}

function makeObject<D, M>(config: ObjectConfigDesc<D, M>): D & M {
  let data = config?.data || {}
  let methods = config?.methods || {}
  return {
    ...data,
    ...methods
  } as D & M
}

At this point, the type of the options object is already type-safe. But the this object in moveBy, which we are most concerned about, will still report type warnings, but we know that in actual operation, the this object in moveBy can already get x and y. The last step is explicitly tell TypeScript this this The real type of the object is , which is very simple, using ThisType:

type ObjectConfigDesc<D, M> = {
  data: D
  methods: M & ThisType<D & M>
}

At this time, it is correct to look at the type hint of options:

let options: {
    x: number;
    y: number;
} & {
    moveBy(dx: number, dy: number): void;
}

You can try in TypeScript Playground, and the experience will be more profound.

Note: ThisType only supports use in the context of object literals, and its use in other places is equivalent to an empty interface.

Four. Concluding remarks

Up to this point, a total of three articles in the "Fun TypeScript Tool Type" series have been completed. The purpose of writing this series is to record some of my learning ideas and feelings in the process of learning, and at the same time to deepen my understanding by writing articles. So if there are any mistakes, criticisms and corrections are welcome.


有道AI情报局
788 声望7.9k 粉丝