typescript - 如何键入 MongoDB 投影结果

标签 typescript mongodb

当我使用投影进行查询时,我希望类型会有所不同。例如,我的集合中有用户类型的文档。

interface User {
    _id?: ObjectId
    username: string
    foo: number
}

当我查询它时,我希望结果是 UserView 类型

interface UserView {
    _id: ObjectId
    username: string
}

我的存储库看起来像这样。

class UserRepository {
    private collection: Collection<User>
    
    constructor(db: Db) {
        this.collection = db.collection<User>('User')
    }

    public async getUserById(id: ObjectId): Promise<UserView | null> {
        // Result has type User
        const result = await this.collection
            .findOne({ _id: ${ eq: id } }, { projection: { _id: 1, username: 1 })
    }
}

目前我能想到的解决方案是像这样为每种类型创建一个新集合。

this.collection = db.collection<User>('User')
this.viewCollection = db.collection<UserView>('User')

或者省略输入并像这样返回函数:

    public async getUserById(id: ObjectId): Promise<UserView | null> {
        // Result has type Document
        const result = await this.collection
            .findOne({ _id: ${ eq: id } }, { projection: { _id: 1, username: 1 })
        if (result) return result as Userview
        return null
    }

在带有投影的 MongoDB 查询中正确输入的最佳方法是什么?

最佳答案

首先,您不需要为您可能使用的每个投影组合创建自定义类型。而不是声明 UserView您可以使用内置的 Pick 实用程序类型:Pick<User,'_id'|'username'> .

然后至于用户投影本身的类型安全检索,而不是使用 as 的更容易出错的类型转换,您可以通过类型参数推断和 keyof 充分利用泛型运算符(operator)达到这样的目的:

public async getUserById<Field extends keyof User>(
    id: mongodb.ObjectId,
    fields: Field[]
): Promise<Pick<User, Field> | null> {   
    const projection: mongodb.FindOptions['projection'] = {};
    fields.forEach((field) => { projection[field] = 1; });
    const result = await this.db.collection<User>('users').findOne(
      { _id: id },
      { projection }
    );
    return result;
}

// Usage:
const user = await this.getUserById('myId', ['_id', 'username']);
console.log(user); // user is fully type-safe with only the _id and username fields

详细解释:

我迭代地达到了以前的解决方案,你可以看到我在 getUserById 的各个版本上的进步下面的方法,如果你想更好地理解它可能会有用:

import * as mongodb from 'mongodb';

interface User {
  _id: string,
  username: string,
  foo: string
}

export default abstract class Test {

  private static db: mongodb.Db;

  public static async init() {
    const client = new mongodb.MongoClient('YOUR_MONGO_URL');
    await client.connect();
    this.db = client.db();
  }

  public static async test() {
    const user: User = { _id: 'myId', username: 'chris', foo: 'bar' };
    console.log(user);

    // Iteration 1: Using hardcoded union type and projection (not reusable)
    const user1 = await this.getUserById1('myId');
    console.log(user1);

    // Iteration 2: Using hardcoded projection (not reusable)
    const user2 = await this.getUserById2<'_id' | 'username'>('myId');
    console.log(user2);

    // Interation 3: Using dynamic union type and projection, but duplicated (not ideal) 
    const user3 = await this.getUserById3<'_id' | 'username'>('myId', ['_id', 'username']);
    console.log(user3);

    // Iteration 4: Using dynamic projection with generic type argument inference, the best solution!
    const user4 = await this.getUserById4('myId', ['_id', 'username']);
    console.log(user4);
  }

  // Iteration 1: Using hardcoded union type and projection (not reusable)
  public static async getUserById1(
    id: string
  ): Promise<Pick<User, '_id' | 'username'> | null> {
    const result = await this.db.collection<User>('users').findOne(
      { _id: id },
      { projection: { _id: true, username: true } }
    );
    return result;
  }

  // Iteration 2: Using hardcoded projection (not reusable)
  public static async getUserById2<Field extends keyof User>(
    id: string
  ): Promise<Pick<User, Field> | null> {
    const result = await this.db.collection<User>('users').findOne(
      { _id: id },
      { projection: { _id: true, username: true } }
    );
    return result;
  }

  // Interation 3: Using dynamic union type and projection, but duplicated (not ideal) 
  public static async getUserById3<Field extends keyof User>(
    id: string,
    fields: (keyof User)[]
  ): Promise<Pick<User, Field> | null> {   
    const projection: mongodb.FindOptions['projection'] = {};
    fields.forEach((field) => { projection[field] = true; });
    const result = await this.db.collection<User>('users').findOne(
      { _id: id },
      { projection }
    );
    return result;
  }

  // Iteration 4: Using dynamic projection with generic type argument inference, the best solution!
  public static async getUserById4<Field extends keyof User>(
    id: string,
    fields: Field[]
  ): Promise<Pick<User, Field> | null> {   
    const projection: mongodb.FindOptions['projection'] = {};
    fields.forEach((field) => { projection[field] = true; });
    const result = await this.db.collection<User>('users').findOne(
      { _id: id },
      { projection }
    );
    return result;
  }

}

关于typescript - 如何键入 MongoDB 投影结果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69865961/

相关文章:

mongodb - MongoDB 的错误距离计算

javascript - 如何从 Angular 2 组件访问外部 javascript 文件

javascript - 将 CSS 应用于事件路由器链接 [Angular 2]

MongoDB 奇怪的 writeResult 行为

MongoDB 嵌套数组查询

mysql - MongoDB: map /单位为浏览器游戏中的模型设计提供建议

javascript - 当我返回到上一个组件时,Angular 重新初始化组件

typescript - 当使用enum作为对象时, "Element implicitly has an ' any'类型,因为索引表达式不是类型 'number'”,为什么?

Typescript : recursive template literal type, 允许字符串和特定链接

mongodb - Golang mongodb 字段级加密