You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript中MongoDB类型安全查询封装的投影问题及解决方法

TypeScript + MongoDB 类型安全查询封装:实现投影的类型缩小

我正在开发一个基于MongoDB的TypeScript项目,目标是为所有数据库查询构建类型安全的通用封装器。最初通过条件类型实现了集合Schema的匹配,但无法定义能缩小过滤器和函数返回值类型范围的投影逻辑。

2023年12月22日更新:已通过函数重载解决该问题,完整实现代码如下:

import { Filter, MongoClient, WithId } from 'mongodb'

// 集合1的数据结构定义
interface Alerts {
  message: string
  severity: number
}
// 集合2的数据结构定义
interface Animals {
  type: string
  legs: number
}

// 根据集合名称匹配对应的数据类型
type CollectionType<T> = T extends { collectionName: 'alerts' } ? Alerts : 
  T extends { collectionName: 'animals' } ? Animals : never
// 带_id的完整文档类型
type TSchema<T> = WithId<CollectionType<T>>
// 投影对象类型定义
type Projection<T> = { [Property in keyof TSchema<T>]?: 0 | 1 }
// 投影字段数组类型
type Fields<T> = (keyof TSchema<T>)[]

// 不带投影的查询参数接口
interface GetWithoutProjection<T> {
  collectionName: 'alerts' | 'animals'
  filter: Filter<TSchema<T>>
}
// 带投影的查询参数接口(继承基础参数)
interface GetWithProjection<T> extends GetWithoutProjection<T> {
  project: Fields<T>
}
// 查询参数联合类型
type GetParams<T> = GetWithProjection<T> | GetWithoutProjection<T>

// 函数重载类型定义
type GetOneFunction = {
  <T extends GetWithProjection<T>>(params: T): Promise<Pick<TSchema<T>, T['project'][number]>>
  <T extends GetWithoutProjection<T>>(params: T): Promise<TSchema<T>>
}

const getOne: GetOneFunction = async <T extends GetParams<T>>(params: T) => {
  const { filter, collectionName } = params

  const client = await MongoClient.connect('connection-string')
  const collection = client.db('db-name').collection<TSchema<T>>(collectionName)

  const projection: Projection<T> = {}
  if ('project' in params) {
    projection._id = 0
    params.project.forEach(p => {
      projection[p] = 1
    })
  }

  const result = await collection.findOne(filter, { projection })
  if (!result) throw new Error(`在集合${collectionName}中未找到匹配${JSON.stringify(filter)}的文档`)
  return result
}

export const test = async () => {
  // 类型为 WithId<Alerts>
  const alerts1 = await getOne({ collectionName: 'alerts', filter: {} })
  console.log(alerts1.severity)
  // 类型为 WithId<Animals>
  const animal1 = await getOne({ collectionName: 'animals', filter: {} })
  console.log(animal1.type)

  // 类型为 Pick<WithId<Alerts>, '_id' | 'message'>
  const alerts2 = await getOne({ collectionName: 'alerts', project: ['_id', 'message'], filter: {} })
  console.log(alerts2.severity)
  // 编译报错:Property 'severity' does not exist on type 'Pick<TSchema<{ collectionName: "alerts"; project: ("_id" | "message")[]; filter: {}; }>, "_id" | "message">'.

  // 类型为 Pick<WithId<Animals>, '_id' | 'legs'>
  const animal2 = await getOne({ collectionName: 'animals', project: ['_id', 'legs'], filter: {} })
  console.log(animal2.type)
  // 编译报错:Property 'type' does not exist on type 'Pick<TSchema<{ collectionName: "animals"; project: ("_id" | "legs")[]; filter: {}; }>, "_id" | "legs">'
}

实现要点

  • 集合Schema自动匹配:通过CollectionType条件类型,根据collectionName参数自动关联对应集合的数据结构
  • 投影的类型约束:利用函数重载区分带/不带投影的调用场景,带投影时通过Pick工具类型精准推导返回值的字段范围,访问未投影字段会触发编译报错
  • 类型安全的查询逻辑:所有参数和返回值都受TypeScript类型校验约束,避免运行时因字段错误导致的问题

内容的提问来源于stack exchange,提问作者John

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 10:43:19