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 不支持通过参数的方式取 statestategetters 都是通过 this 去取就解决了。但这也是猜想,这也不是这篇文章的重点。在研究这个问题的时候,发现了 TypeScript 中的 this 关键字非常有趣。然后就特意拿出来分享一下。

  1. JavaScript 中的 this

默认情况下,普通函数(非箭头函数)内部 this 的值取决于函数的调用方式。在下面例子中,因为函数是通过 dog 引用调用的,它的值 thisdog 而不是 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 关键字变得特别有趣。

  1. 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);

我们将 AnimalDog 改成继承关系,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

  1. 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中声明的一个空接口。除了在对象文字的上下文类型中被识别之外,该接口的行为类似于任何空接口。

  1. 函数参数中的 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);
  1. 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);
}
全部评论

相关推荐

字节 飞书绩效团队 (n+2) * 15 + 1k * 12 + 1w
点赞 评论 收藏
分享
10-14 23:01
已编辑
中国地质大学(武汉) Java
CUG芝士圈:虽然是网上的项目,但最好还是包装一下,然后现在大部分公司都在忙校招,十月底、十一月初会好找一些。最后,boss才沟通100家,别焦虑,我去年暑假找第一段实习的时候沟通了500➕才有面试,校友加油
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务