TypeScript 中的静态

¥Statics in TypeScript

要使用 Mongoose 的自动类型推断来定义 staticsmethods 的类型,你应该使用 methodsstatics 结构选项定义方法和静态,如下所示。请勿使用 Schema.prototype.method()Schema.prototype.static() 函数,因为 Mongoose 的自动类型推断系统无法检测使用这些函数定义的方法和静态变量。

¥To use Mongoose's automatic type inference to define types for your statics and methods, you should define your methods and statics using the methods and statics schema options as follows. Do not use the Schema.prototype.method() and Schema.prototype.static() functions, because Mongoose's automatic type inference system cannot detect methods and statics defined using those functions.

const userSchema = new mongoose.Schema(
  { name: { type: String, required: true } },
  {
    methods: {
      updateName(name: string) {
        this.name = name;
        return this.save();
      }
    },
    statics: {
      createWithName(name: string) {
        return this.create({ name });
      }
    }
  }
);
const UserModel = mongoose.model('User', userSchema);

const doc = new UserModel({ name: 'test' });
// Compiles correctly
doc.updateName('foo');
// Compiles correctly
UserModel.createWithName('bar');

使用泛型

¥With Generics

我们建议尽可能使用 Mongoose 的自动类型推断,但你可以使用 SchemaModel 泛型为你的静态和方法设置类型推断。Mongoose models 没有 statics 的显式通用参数。如果你的模型有静态,我们建议创建一个 extends Mongoose 的 Model 接口的接口,如下所示。

¥We recommend using Mongoose's automatic type inference where possible, but you can use Schema and Model generics to set up type inference for your statics and methods. Mongoose models do not have an explicit generic parameter for statics. If your model has statics, we recommend creating an interface that extends Mongoose's Model interface as shown below.

import { Model, Schema, model } from 'mongoose';

interface IUser {
  name: string;
}

interface UserModelType extends Model<IUser> {
  myStaticMethod(): number;
}

const schema = new Schema<IUser, UserModelType>({ name: String });
schema.static('myStaticMethod', function myStaticMethod() {
  return 42;
});

const User = model<IUser, UserModelType>('User', schema);

const answer: number = User.myStaticMethod(); // 42

你应该将方法作为第三个通用参数传递给 Schema 构造函数,如下所示。

¥You should pass methods as the 3rd generic param to the Schema constructor as follows.

import { Model, Schema, model } from 'mongoose';

interface IUser {
  name: string;
}

interface UserMethods {
  updateName(name: string): Promise<any>;
}

const schema = new Schema<IUser, Model<IUser>, UserMethods>({ name: String });
schema.method('updateName', function updateName(name) {
  this.name = name;
  return this.save();
});

const User = model('User', schema);
const doc = new User({ name: 'test' });
// Compiles correctly
doc.updateName('foo');

loadClass() 与 TypeScript 结合使用

¥Using loadClass() with TypeScript

Mongoose 支持使用 schema.loadClass() 将 ES6 类应用到模式,作为在模式中定义静态变量和方法的替代方案。使用 TypeScript 时,需要了解一些重要的类型细节。

¥Mongoose supports applying ES6 classes to a schema using schema.loadClass() as an alternative to defining statics and methods in your schema. When using TypeScript, there are a few important typing details to understand.

基本用法

¥Basic Usage

loadClass() 会将类中的静态方法、实例方法和 ES getter/setter 复制到 schema 中。

¥loadClass() copies static methods, instance methods, and ES getters/setters from the class onto the schema.

class MyClass {
  myMethod() {
    return 42;
  }

  static myStatic() {
    return 42;
  }

  get myVirtual() {
    return 42;
  }
}

const schema = new Schema({ property1: String });
schema.loadClass(MyClass);

Mongoose 不会自动更新类成员的 TypeScript 类型。要获得完整的类型支持,你必须使用 Mongoose 的 模型HydratedDocument 泛型手动定义类型。

¥Mongoose does not automatically update TypeScript types for class members. To get full type support, you must manually define types using Mongoose's Model and HydratedDocument generics.

// 1. Define an interface for the raw document data
interface RawDocType {
  property1: string;
}

// 2. Define the Model type
// This includes the raw data, query helpers, instance methods, virtuals, and statics.
type MyCombinedModel = Model<
  RawDocType, 
  {}, 
  Pick<MyClass, 'myMethod'>, 
  Pick<MyClass, 'myVirtual'> 
> & Pick<typeof MyClass, 'myStatic'>; 

// 3. Define the Document type
type MyCombinedDocument = HydratedDocument<
  RawDocType,
  Pick<MyClass, 'myMethod'>, 
  {}, 
  Pick<MyClass, 'myVirtual'> 
>;

// 4. Create the Mongoose model
const MyModel = model<RawDocType, MyCombinedModel>(
  'MyClass',
  schema
);

MyModel.myStatic();
const doc = new MyModel();
doc.myMethod();
doc.myVirtual;
doc.property1;     

在方法内部定义 this 类型

¥Typing this Inside Methods

你可以在方法中使用你定义的 模型HydratedDocument 类型,通过注解 this 来启用完全安全性。请注意,必须对每个方法单独执行此操作;无法一次性为整个类设置 this 类型。

¥You can annotate this in methods to enable full safety, using the Model and HydratedDocument types you defined. Note that this must be done for each method individually; it is not possible to set a this type for the entire class at once.

class MyClass {
  // Instance method typed with correct `this` type
  myMethod(this: MyCombinedDocument) {
    return this.property1;
  }

  // Static method typed with correct `this` type
  static myStatic(this: MyCombinedModel) {
    return 42;
  }
}

Getter/Setter 限制

¥Getters / Setters Limitation

TypeScript 目前不允许在 getter/setter 中使用 this 参数:

¥TypeScript currently does not allow this parameters on getters/setters:

class MyClass {
  // error TS2784: 'this' parameters are not allowed in getters
  get myVirtual(this: MyCombinedDocument) {
    return this.property1;
  }
}

这是 TypeScript 的限制。参见:TypeScript 问题 #52923

¥This is a TypeScript limitation. See: TypeScript issue #52923

作为一种变通方法,你可以在 getter 中将 this 强制转换为文档类型:

¥As a workaround, you can cast this to the document type inside your getter:

get myVirtual() {
  // Workaround: cast 'this' to your document type
  const self = this as MyCombinedDocument;
  return `Name: ${self.property1}`;
}

完整示例代码

¥Full Example Code

import { Model, Schema, model, HydratedDocument } from 'mongoose';

interface RawDocType {
  property1: string;
}

class MyClass {
  myMethod(this: MyCombinedDocument) {
    return this.property1;
  }

  static myStatic(this: MyCombinedModel) {
    return 42;
  }

  get myVirtual() {
    const self = this as MyCombinedDocument;
    return `Hello ${self.property1}`;
  }
}

const schema = new Schema<RawDocType>({ property1: String });
schema.loadClass(MyClass);

type MyCombinedModel = Model<
  RawDocType,
  {},
  Pick<MyClass, 'myMethod'>,
  Pick<MyClass, 'myVirtual'>
> & Pick<typeof MyClass, 'myStatic'>;

type MyCombinedDocument = HydratedDocument<
  RawDocType,
  Pick<MyClass, 'myMethod'>,
  {},
  Pick<MyClass, 'myVirtual'>
>;

const MyModel = model<RawDocType, MyCombinedModel>(
  'MyClass',
  schema
);

const doc = new MyModel({ property1: 'world' });
doc.myMethod(); 
MyModel.myStatic(); 
console.log(doc.myVirtual); 

何时应该使用 loadClass()

¥When Should I Use loadClass()?

loadClass() 可用于在类中定义方法和静态变量。如果你强烈偏好类,可以使用 loadClass();但是,我们建议你按照第一节所述,在模式选项中定义 staticsmethods

¥loadClass() is useful for defining methods and statics in classes. If you have a strong preference for classes, you can use loadClass(); however, we recommend defining statics and methods in schema options as described in the first section.

TypeScript 中 loadClass() 的主要缺点是需要手动定义 TypeScript 类型。如果你需要更好的类型推断,可以使用模式选项 methodsstatics

¥The major downside of loadClass() in TypeScript is that it requires manual TypeScript types. If you want better type inference, you can use schema options methods and statics.