如何使用TypeScript映射ElasticSearch响应并解决TS2339类型报错
现有项目信息
依赖版本
"@nestjs/elasticsearch": "^8.1.0", "@elastic/elasticsearch": "^8.2.1", "@types/elasticsearch": "^5.0.40",
自定义Post相关接口定义
export interface PostSearchResultInterface { hits: { total: { value: number; }; hits: Array<{ total: { value: number; } _source: PostSearchBodyInterface; }>; }; }
业务实现代码
const search = await this.elasticsearchService.search<PostSearchBodyInterface>({ index: this.index, from: offset, size: limit, body: { query: { bool: { should: { multi_match: { query: text, fields: ['title', 'story', 'other_titles'] } }, filter: { range: { id: { gte: startId } } } }, }, sort: { id: { order: 'asc' } } } }) ; // 此处报错 let count = search.hits.total.value const hits = search.hits.hits; const results = hits.map((item) => item._source); return { count : startId ? separateCount : count, results }
报错信息
TS2339类型错误:Property 'value' does not exist on type 'number | SearchTotalHits'. Property 'value' does not exist on type 'number'.
报错位置:let count = search.hits.total.value
报错原因
ElasticSearch 8.x官方客户端的hits.total默认是number | SearchTotalHits联合类型:
- 当查询配置
rest_total_hits_as_int: true时,hits.total直接返回数字格式的总命中数 - 默认配置下
hits.total是对象结构,包含value(总数值)和relation(计数规则)两个字段
当前代码调用search方法时仅传入了文档内容的泛型PostSearchBodyInterface,TS自动使用客户端内置的联合类型做类型校验,直接访问.value属性时,因为联合类型中number类型不存在该属性,就会抛出TS2339错误。
另外自定义的PostSearchResultInterface存在两个问题:一是没有实际传入泛型使用,二是内层hits数组的元素不需要重复定义total字段。
修复方案
三种方案可按需选择:
方案1:类型兼容处理(无需修改查询参数)
对hits.total做类型判断,兼容两种返回格式,不需要额外自定义接口:
const search = await this.elasticsearchService.search<PostSearchBodyInterface>({ index: this.index, from: offset, size: limit, // 原有查询参数保持不变 body: { query: { bool: { should: { multi_match: { query: text, fields: ['title', 'story', 'other_titles'] } }, filter: { range: { id: { gte: startId } } } }, }, sort: { id: { order: 'asc' } } } }); // 兼容两种total类型 const count = typeof search.hits.total === 'number' ? search.hits.total : search.hits.total.value; const hits = search.hits.hits; const results = hits.map((item) => item._source); return { count : startId ? separateCount : count, results }
方案2:配置参数让total直接返回数字
在查询参数中添加rest_total_hits_as_int: true,此时hits.total会直接返回数字类型,不需要访问.value属性:
const search = await this.elasticsearchService.search<PostSearchBodyInterface>({ index: this.index, from: offset, size: limit, rest_total_hits_as_int: true, // 新增该配置 body: { // 原有查询体保持不变 } }); // 直接取值即可,不需要.value const count = search.hits.total; const hits = search.hits.hits; const results = hits.map((item) => item._source); return { count : startId ? separateCount : count, results }
方案3:使用自定义接口作为返回类型
先修正接口定义错误,再把接口作为第二个泛型参数传入search方法:
首先修正接口:
interface PostSearchResultInterface { hits: { total: { value: number; relation?: string; }; hits: Array<{ _source: PostSearchBodyInterface; }>; }; }
调用时指定返回类型:
// 第二个泛型参数传入自定义的返回结构类型 const search = await this.elasticsearchService.search<PostSearchBodyInterface, PostSearchResultInterface>({ // 查询参数不变 }); // 此时TS会识别total为带value的对象类型,不会报错 const count = search.hits.total.value;
注意:该方案需要确认ES查询配置下hits.total确实返回对象格式,否则运行时会出现取值错误。
内容的提问来源于stack exchange,提问作者Kareem Adel

