JS/TS根据单个过滤对象属性值过滤对象数组
我需要对API调用返回的结果集进行过滤,该结果集为强类型对象构成的数组,下文称其为resultsArray。
过滤所用的条件来自另一个对象,下文称其为filterObject,该对象的属性名与resultsArray中对象的属性名完全一致。过滤逻辑为:遍历resultsArray时,若数组项在两个对象共有的属性上的值与filterObject对应属性值完全匹配,则保留该数组项。
实际业务中,过滤条件来自UI表单,规则如下:
- 若
filterObject中未设置任何有效条件(所有属性值为null),则返回并展示全部resultsArray数据 - 若设置了部分条件,则仅展示匹配所有已设置条件的记录
例如下方的filterObject:
const filterObject = { usdotnumber: '13', mcnumber: null, mxnumber: null, ffnumber: null, vatnumber: '12345', nvocc: null, };
上述示例中,仅需要保留usdotnumber = 13 且 vatnumber = 12345的记录。
尝试过多种公开方案,但大多是对象与对象、数组与数组的比对,没有找到单个对象与对象数组做过滤匹配的合适方案,实现过程中遇到了「key不可索引」的报错:因为filterObject是单对象而非可索引的数组/类数组结构,无法直接用字符串键索引取值。
补充说明1
具体报错为字符串无法作为类型索引,报错信息如下:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'filters'. No index signature with a parameter of type 'string' was found on type 'filters'.
触发报错的代码片段如下:
processData(carriers: MultiCarrierResponse): void { interface filters { ffnumber: string | null; mcnumber: string | null; mxnumber: string | null; nvocc: string | null; usdotnumber: string | null; vatnumber: string | null; } const filterObject: filters = { usdotnumber: '13', mcnumber: null, mxnumber: null, ffnumber: null, vatnumber: '12345', nvocc: null, }; const filteredResults = carriers?.carriers?.filter((o) => Object.keys(filterObject).every((k) => filterObject[k] === o[k]), ); console.log('The filtered results: ', filteredResults); carriers?.carriers?.forEach((element) => { this.values.push({ carrierId: element.carrierId, carrierName: element.carrierName, carrierStatus: element.carrierStatus, }); }); console.log('The carriers: ', carriers); }
补充说明2
调整为如下代码后解决了TS类型报错:
filterByObject(values: any[] | null | undefined, filterObject: any): any { const filterKeys = Object.keys(filterObject).filter( (key) => filterObject[key] !== null, ); return values?.filter((value: { [x: string]: any }) => { return filterKeys.every((key) => filterObject[key] === value[key]); }); }
该代码可通过TS类型校验,但仍存在逻辑问题:无法正确处理resultsArray中对应filterObjectnull值属性的匹配逻辑,需要完善实现方案。
核心修复点
- 提前提取
filterObject中值不为null的有效键值对,null代表不设置该字段的过滤条件,不参与匹配逻辑 - 用TS泛型做类型约束,不使用
any绕开类型检查,从根源解决字符串索引报错,同时保留完整类型提示 - 增加边界判断:如果没有有效过滤条件,直接返回全量原数组,减少不必要的遍历
- 修复原代码逻辑bug:过滤完成后不要遍历原数组处理数据,直接使用过滤后的结果
通用过滤函数实现
/** * 按单对象条件过滤对象数组 * @param values 待过滤的对象数组 * @param filterObject 过滤条件对象,值为null的属性不参与过滤 * @returns 过滤后的数组 */ function filterByObject<T extends Record<string, any>>( values: T[] | null | undefined, filterObject: Partial<Record<keyof T, T[keyof T] | null>> ): T[] { // 源数组为空时直接返回空数组 if (!values) return []; // 提取所有非null的有效过滤条件 const validFilterEntries = Object.entries(filterObject).filter( ([, value]) => value !== null ) as [keyof T, T[keyof T]][]; // 无有效过滤条件,返回全量数据 if (validFilterEntries.length === 0) { return [...values]; } // 匹配所有有效条件的项才保留 return values.filter((item) => { return validFilterEntries.every(([key, targetValue]) => item[key] === targetValue); }); }
业务场景调用示例
对应原processData方法的改造:
processData(carriers: MultiCarrierResponse): void { // 对齐接口返回结构定义数据类型 interface Carrier { ffnumber: string | null; mcnumber: string | null; mxnumber: string | null; nvocc: string | null; usdotnumber: string | null; vatnumber: string | null; carrierId: string | number; carrierName: string; carrierStatus: string | number; } const filterObject: Partial<Record<keyof Carrier, Carrier[keyof Carrier] | null>> = { usdotnumber: '13', mcnumber: null, mxnumber: null, ffnumber: null, vatnumber: '12345', nvocc: null, }; // 调用通用过滤方法拿到结果 const filteredResults = filterByObject<Carrier>(carriers?.carriers, filterObject); console.log('The filtered results: ', filteredResults); // 基于过滤后的结果做后续处理 this.values = filteredResults.map((element) => ({ carrierId: element.carrierId, carrierName: element.carrierName, carrierStatus: element.carrierStatus, })); console.log('The carriers: ', carriers); }
原代码问题说明
- 最早的TS索引报错:
Object.keys()默认返回string[]类型,TS不允许用宽泛的string类型索引没有定义字符串索引签名的自定义接口,通过泛型约束+类型收窄即可解决,不需要用any丢失类型安全 - 之前调整的版本问题:没有处理「无有效过滤条件返回全量」的边界,且全量使用
any丢失了类型提示,容易写出隐藏bug - 原业务代码逻辑bug:计算出
filteredResults后仍然遍历原全量数组做数据转换,导致过滤逻辑完全不生效,改造后直接基于过滤结果做转换即可。
内容的提问来源于stack exchange,提问作者rcastagna

