简体   繁体   中英

Using a Map instance to return an array of distinct objects by property name?

This implementation seems to be working fine ( Stackblitz ):

    /**
     * Returns all the elements that are distinct by the 
     * `property` value.  Note that the implementation uses a `Map<string, E>` to
     * index the entities by key.  Therefore the more recent occurences 
     * matching a key instance will overwrite the previous ones.
     * 
     * @param property The name of the property to check for distinct values by.
     * @param entities The entities in the array.
     * 
     * @example
     * ```
     * let todos:Todo = [{ id: 1, "Lets do it!" }, {id: 2, "All done!"}];
     * let dtodos:Todo[] = distinct<Todo>(todos, 'id');
     */
    export function distinct<E>(entities:E[], property:string):E[] {
        let map:Map<string, E> = new Map();
        entities.forEach((e:E)=>{
            map.set(e[property], e);
        });
        return Array.from(map.values());
    }

The only thing is that VSCode draws a red squiggly under the e[property] part and the error message is:

Element implicitly has an 'any' type because type '{}' has no index signature.ts(7017)

Is there a way to get rid of that?

Library Holding Implementation

I added the latest suggested implementation to this light weight state manager for objects and entities:

https://www.npmjs.com/package/@fireflysemantics/slice

npm i @fireflysemantics/slice
...
import {distinct} from '@fireflysemantics/slice/utilities';

Demo

https://stackblitz.com/edit/typescript-slice-distinct

The Error message is a bit misleading. His problem is, that it can not ensure that e[property] is of type string as you've defined the Map .

Make the key in the Map of type any as with so much flexibility you can not determine the type of the value either.

Additionally I'd type the property parameter as keyof E so TS ensures that I can only paste in valid property names for the type.

function distinct<E>(entities:E[], property:keyof E):E[] {
    let map:Map<any, E> = new Map();
    entities.forEach((e:E)=>{
        map.set(e[property], e);
    });
    return Array.from(map.values());
}

Based on Thomas ' answer, we can simplify both:

  • JavaScript code: constructing the Map at once;
  • TypeScript typing: adding the K extends keyof E , we can cast the tuples ( [E[K], E] ) used as Map constructor input parameter and remove the any type use.

Here the code:

function distinct<E, K extends keyof E>(entities: E[], property: K): E[] {
    const entitiesByProperty = new Map(entities.map(e => [e[property], e] as [E[K], E]));
    return Array.from(entitiesByProperty.values());
}

When calling distinct() , there's no need to specify the generic types since they can be inferred. Here's a working example:

enum Status { Pending = 0, Done = 1 }
interface Todo { id: number, label: string, status: Status, date?: Date }
const todos: Todo[] = [
    { id: 1, label: 'Task 1', status: Status.Pending },
    { id: 2, label: 'Task 2', status: Status.Pending },
    { id: 1, label: 'Task 1', status: Status.Done },
];
distinct(todos, 'id'); // [{ id: 1, ... status: 1 }, { id: 2, ... status: 0 }]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM