TypeScript 中的 this
起因
使用 pinia 中的 defineStore
时,类型推导不出来,this.test1
应该是 number
类型,但是推导出来的类型却是 () => number
。
然后给 test2
指定返回值类型,this.test1
就可以自动推导出来了。
也不知道是为什么?文档说是 TypeScript 已知的缺陷。然后就去翻了一下 pinia 的类型定义,最后简化了一下类型定义,代码大致有变成了这个样子。
export declare type _StoreWithGetters<G> = {
readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : G[k];
};
interface DefineStoreOptions<G> {
getters: G & ThisType<_StoreWithGetters<G>> & Record<string, ((state: any) => any) | (() => any)>;
}
function defineStore<T>(options: DefineStoreOptions<T>) {}
如果将 DefineStoreOptions
修改一下,就不会报错了。
interface DefineStoreOptions<G> {
getters: G & ThisType<_StoreWithGetters<G>>;
}
所以如果 getters
不支持通过参数的方式取 state
,state
和 getters
都是通过 this
去取就解决了。但这也是猜想,这也不是这篇文章的重点。在研究这个问题的时候,发现了 TypeScript 中的 this
关键字非常有趣。然后就特意拿出来分享一下。
-
JavaScript 中的
this
默认情况下,普通函数(非箭头函数)内部 this
的值取决于函数的调用方式。在下面例子中,因为函数是通过 dog
引用调用的,它的值 this
是 dog
而不是 animal
。
const animal = {
name: 'animal',
getName() {
console.log(this.name);
}
}
const dog = {
name: 'dog',
getName: animal.getName,
}
// dog, 不是 animal
dog.getName();
JavaScript 中的 this
很多时候,是运行的时候才能确定的,但是 TypeScript 是在编译的时候确定 this
的类型。众所周知,TypeScript 是不会修改 JavaScript 的行为的,所以这种冲突使得 this
关键字变得特别有趣。
-
this
的类型推导
普通函数中的 this
class Animal {
name = 'animal';
getThis() {
return this;
}
}
const animal = new Animal();
class Dog {
name = 'dog';
getThis = animal.getThis;
}
const dog = new Dog();
// TypeScript 推导出 dog2 的类型是 Animal
const dog2 = dog.getThis();
// 但 name 实际打印出的是 dog
console.log(dog2.name);
在上面的例子中,因为 this
是运行的时候才能确定的,所以 dog.getThis()
应该是 dog
,但是因为 TypeScript 是编译的时候确认 this
的类型,所以就产生了 TypeScript 中推导的类型和实际类型不一致的情况。
箭头函数中的 this
class Animal {
name = 'animal';
getThis = () => {
return this;
}
}
const animal = new Animal();
class Dog {
name = 'dog';
getThis = animal.getThis;
}
const dog = new Dog();
// TypeScript 推导出 dog2 的类型是 Animal
const dog2 = dog.getThis();
// name 实际打印出来的是 animal
console.log(dog2.name);
因为箭头函数中的 this
是定义的时候确定的,TypeScript 推导出来的类型和实际的类型是一致的,都是 animal
。但是箭头函数也不是完美的,不能在派生类中使用 super.getThis
,因为原型链中没有条目可以从中获取基类方法。
继承中的 this
class Animal {
name = 'animal';
getThis = () => {
return this;
}
}
class Dog extends Animal {
name = 'dog';
}
const dog = new Dog();
// TypeScript 推导出 dog2 的类型是 Dog
const dog2 = dog.getThis();
// name 实际打印出来的是 dog
console.log(dog2.name);
我们将 Animal
和 Dog
改成继承关系,TypeScript 推导出来的类型和实际的类型是一致的,都是 Dog
。
类型声明后的 this
class Animal {
name = 'animal';
getThis = () => {
return this;
}
}
class Dog extends Animal {
name = 'dog';
}
const dog: Animal = new Dog();
// TypeScript 推导出 dog2 的类型是 Animal
const dog2 = dog.getThis();
// 但是 name 实际打印出来的是 dog
console.log(dog2.name);
我们将 dog
的类型声明成 Animal
,此时我们的类型声明的优先级要高于 TypeScript 的自动推导,所以他们会推导出来 dog2
的类型是 Animal
,但是 dog2
的实际类型是 Dog
。
-
ThisType
使用ThisType
需要将 noImplicitThis
设置为 true
。
class Animal {
name = 'animal';
getThis() {
return this;
}
}
class Dog extends Animal {
name = 'dog';
}
const animal: Animal & ThisType<Dog> = {
name: 'animal',
// this 变成 Dog
getThis() {
return this;
}
};
我们使用 ThisType<Dog>
后,animal.getThis
中的 this
会被推断成 Dog
。
应用
我们可以使用 ThisType
重写方法中的 this
类型,这是一个非常好用的功能。例如 pinia 和 vue 中都用到了个功能,下面的这个例子,是 ThisType
最常见的应用。
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}
let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this
},
},
});
在上面的例子中,makeObject
的参数中的方法对象具有包含 ThisType<D&M>
的上下文类型,因此方法对象中的 this
的类型是 {x: number,y:number } & { moveBy(dx:number,dy:number):void}
。 ThisType<T>
标记接口只是在lib. d.ts中声明的一个空接口。除了在对象文字的上下文类型中被识别之外,该接口的行为类似于任何空接口。
-
函数参数中的
this
this
用作类型
class Animal {
name = 'animal';
compare(other: this) {
return this.name === other.name;
}
}
我们可以将 this
当做一个类型使用,上面的代码等同于将 other: Animal
class Animal {
name = 'animal';
compare(other: Animal) {
return this.name === other.name;
}
}
this
用作值
class Animal {
name = 'animal';
getThis() {
return this;
}
}
const animal = new Animal();
const getThis = animal.getThis;
// 打印结果是 undefined,但是推导的类型是 Animal。
console.log(getThis());
上面的打印结果是 undefined
,这肯定不是我们想要的。如果我们不想出现这种情况,除了使用箭头函数,我们还有另一种方式,将 this
当做参数
class Animal {
name = 'animal';
getThis(this: Animal) {
return this;
}
}
/** Animal 的实际编译结果,会把 this 去掉,但是会在检查类型的时候报错
class Animal {
constructor() {
this.name = 'animal';
}
getThis() {
return this;
}
}
**/
const animal = new Animal();
const getThis = animal.getThis;
// 报错,The 'this' context of type 'void' is not assignable to method's 'this' of type 'Animal'.
console.log(getThis());
call
和 apply
调用
class Animal {
name = 'animal';
getThis(this: Animal) {
return this;
}
}
const dog = {};
const animal = new Animal();
// 报错,Argument of type '{}' is not assignable to parameter of type 'Animal'.
animal.getThis.call(dog);
因为我们在 getThis
中添加 this: Animal
了,所以在使用 call
的时候,需要我们传入的必须是 Animal
类型的值,否则会提示报错。这样可以很好的约束 call
传入的值,防止 TypeScript 类型检查没有检查出来问题,但是在实际调用中报错。
ThisParameterType
ThisParameterType
提取函数类型的 this
参数的类型,如果函数类型没有 this
参数,则为 unknown
。实现方式:
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any ? U : unknown;
我们可以通过 ThisParameterType
获取 this
的类型。
// Animal
type Res = ThisParameterType<typeof animal.getThis>
OmitThisParameter
OmitThisParameter
从 Type 中删除 this
参数。实现方式:
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
移除 this
参数以后,再调用 call
类型就不会报错了。
(animal.getThis as OmitThisParameter<typeof animal.getThis>).call(dog);
-
this
的类型守卫
你可以在类和接口中方法的返回位置使用 this is Type
。当与类型缩小(例如if
语句)混合时,目标对象的类型将缩小为指定的 Type
。
class Animal {
name = 'animal';
isDog(): this is Dog{
return this instanceof Dog
}
}
class Dog extends Animal {
name = 'dog';
}
const animal = new Animal();
if (animal.isDog()) {
// animal 的类型被推导为 Dog
console.log(animal);
} else {
// animal 的类型被推导为 Animal
console.log(animal);
}